MM-45755 - Calls: Unmute automatically in DM or GM channels (#6627)

* unmute in dm or gm channels

* refactor leave_and_join

* waitForReady -> waitForPeerConnection; 10ms -> 200ms check frequency
This commit is contained in:
Christopher Poile
2022-09-08 13:40:55 -04:00
committed by GitHub
parent 9ab4c935ef
commit 38b68733d0
12 changed files with 133 additions and 106 deletions

View File

@@ -68,7 +68,7 @@ jest.mock('@calls/connection/connection', () => ({
disconnect: jest.fn(),
mute: jest.fn(),
unmute: jest.fn(),
waitForReady: jest.fn(() => Promise.resolve()),
waitForPeerConnection: jest.fn(() => Promise.resolve()),
})),
}));

View File

@@ -240,7 +240,7 @@ export const joinCall = async (serverUrl: string, channelId: string): Promise<{
}
try {
await connection.waitForReady();
await connection.waitForPeerConnection();
return {data: channelId};
} catch (e) {
connection.disconnect();

View File

@@ -3,6 +3,9 @@
import {Alert} from 'react-native';
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
import {errorAlert} from '@calls/utils';
import type {IntlShape} from 'react-intl';
export const showLimitRestrictedAlert = (maxParticipants: number, intl: IntlShape) => {
@@ -30,3 +33,83 @@ export const showLimitRestrictedAlert = (maxParticipants: number, intl: IntlShap
],
);
};
export const leaveAndJoinWithAlert = (
intl: IntlShape,
serverUrl: string,
channelId: string,
leaveChannelName: string,
joinChannelName: string,
confirmToJoin: boolean,
newCall: boolean,
isDMorGM: boolean,
) => {
if (confirmToJoin) {
const {formatMessage} = intl;
let joinMessage = formatMessage({
id: 'mobile.leave_and_join_message',
defaultMessage: 'You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and join the call in ~{joinChannelName}?',
}, {leaveChannelName, joinChannelName});
if (newCall) {
joinMessage = formatMessage({
id: 'mobile.leave_and_join_message',
defaultMessage: 'You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and start a new call in ~{joinChannelName}?',
}, {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',
}),
},
{
text: formatMessage({
id: 'mobile.leave_and_join_confirmation',
defaultMessage: 'Leave & Join',
}),
onPress: () => doJoinCall(serverUrl, channelId, isDMorGM, intl),
style: 'cancel',
},
],
);
} else {
doJoinCall(serverUrl, channelId, isDMorGM, intl);
}
};
const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolean, intl: IntlShape) => {
const {formatMessage} = intl;
const hasPermission = await hasMicrophonePermission(intl);
if (!hasPermission) {
errorAlert(formatMessage({
id: 'mobile.calls_error_permissions',
defaultMessage: 'No permissions to microphone, unable to start call',
}), intl);
return;
}
const res = await joinCall(serverUrl, channelId);
if (res.error) {
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
errorAlert(res.error?.toString() || seeLogs, intl);
return;
}
if (isDMorGM) {
// FIXME (MM-46048) - HACK
// There's a race condition between unmuting and receiving existing tracks from other participants.
// Fixing this properly requires extensive and potentially breaking changes.
// Waiting for a second before unmuting is a decent workaround that should work in most cases.
setTimeout(() => unmuteMyself(), 1000);
}
};

View File

@@ -6,8 +6,7 @@ import React from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
@@ -30,6 +29,7 @@ type Props = {
currentCallChannelId?: string;
leaveChannelName?: string;
joinChannelName?: string;
joinChannelIsDMorGM?: boolean;
limitRestrictedInfo?: LimitRestrictedInfo;
}
@@ -108,7 +108,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
export const CallsCustomMessage = ({
post, currentUser, author, isMilitaryTime, teammateNameDisplay,
currentCallChannelId, leaveChannelName, joinChannelName, limitRestrictedInfo,
currentCallChannelId, leaveChannelName, joinChannelName, joinChannelIsDMorGM, limitRestrictedInfo,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
@@ -130,7 +130,7 @@ export const CallsCustomMessage = ({
return;
}
leaveAndJoinWithAlert(intl, serverUrl, post.channelId, leaveChannelName || '', joinChannelName || '', confirmToJoin, false);
leaveAndJoinWithAlert(intl, serverUrl, post.channelId, leaveChannelName || '', joinChannelName || '', confirmToJoin, false, Boolean(joinChannelIsDMorGM));
};
if (post.props.end_at) {

View File

@@ -15,6 +15,7 @@ import {getPreferenceAsBool} from '@helpers/api/preference';
import {observeChannel} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentUser, observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
import {isDMorGM} from '@utils/channel';
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
@@ -56,10 +57,15 @@ const enhanced = withObservables(['post'], ({serverUrl, post, database}: OwnProp
switchMap((c) => of$(c ? c.displayName : '')),
distinctUntilChanged(),
);
const joinChannelName = observeChannel(database, post.channelId).pipe(
const joinChannel = observeChannel(database, post.channelId);
const joinChannelName = joinChannel.pipe(
switchMap((chan) => of$(chan?.displayName || '')),
distinctUntilChanged(),
);
const joinChannelIsDMorGM = joinChannel.pipe(
switchMap((chan) => of$(chan ? isDMorGM(chan) : false)),
distinctUntilChanged(),
);
return {
currentUser,
@@ -69,6 +75,7 @@ const enhanced = withObservables(['post'], ({serverUrl, post, database}: OwnProp
currentCallChannelId,
leaveChannelName,
joinChannelName,
joinChannelIsDMorGM,
limitRestrictedInfo: observeIsCallLimitRestricted(serverUrl, post.channelId),
};
});

View File

@@ -5,8 +5,7 @@ import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {leaveCall} from '@calls/actions';
import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import {useTryCallsFunction} from '@calls/hooks';
import OptionBox from '@components/option_box';
import {preventDoubleTap} from '@utils/tap';
@@ -17,6 +16,7 @@ export interface Props {
serverUrl: string;
displayName: string;
channelId: string;
channelIsDMorGM: boolean;
isACallInCurrentChannel: boolean;
confirmToJoin: boolean;
alreadyInCall: boolean;
@@ -29,6 +29,7 @@ const ChannelInfoStartButton = ({
serverUrl,
displayName,
channelId,
channelIsDMorGM,
isACallInCurrentChannel,
confirmToJoin,
alreadyInCall,
@@ -45,7 +46,7 @@ const ChannelInfoStartButton = ({
} else if (isLimitRestricted) {
showLimitRestrictedAlert(limitRestrictedInfo.maxParticipants, intl);
} else {
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, confirmToJoin, !isACallInCurrentChannel);
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, confirmToJoin, !isACallInCurrentChannel, channelIsDMorGM);
}
dismissChannelInfo();

View File

@@ -11,6 +11,7 @@ import {observeIsCallLimitRestricted} from '@calls/observers';
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
import DatabaseManager from '@database/manager';
import {observeChannel} from '@queries/servers/channel';
import {isDMorGM} from '@utils/channel';
import type {WithDatabaseArgs} from '@typings/database/database';
@@ -20,8 +21,13 @@ type EnhanceProps = WithDatabaseArgs & {
}
const enhanced = withObservables([], ({serverUrl, channelId, database}: EnhanceProps) => {
const displayName = observeChannel(database, channelId).pipe(
switchMap((channel) => of$(channel?.displayName || '')),
const channel = observeChannel(database, channelId);
const displayName = channel.pipe(
switchMap((c) => of$(c?.displayName || '')),
distinctUntilChanged(),
);
const channelIsDMorGM = channel.pipe(
switchMap((chan) => of$(chan ? isDMorGM(chan) : false)),
distinctUntilChanged(),
);
const isACallInCurrentChannel = observeChannelsWithCalls(serverUrl).pipe(
@@ -48,6 +54,7 @@ const enhanced = withObservables([], ({serverUrl, channelId, database}: EnhanceP
return {
displayName,
channelIsDMorGM,
isACallInCurrentChannel,
confirmToJoin,
alreadyInCall,

View File

@@ -12,6 +12,7 @@ import {observeCallsState, observeCurrentCall} from '@calls/state';
import {idsAreEqual} from '@calls/utils';
import {observeChannel} from '@queries/servers/channel';
import {queryUsersById} from '@queries/servers/user';
import {isDMorGM} from '@utils/channel';
import type {WithDatabaseArgs} from '@typings/database/database';
@@ -25,10 +26,15 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
channelId,
database,
}: OwnProps & WithDatabaseArgs) => {
const displayName = observeChannel(database, channelId).pipe(
const channel = observeChannel(database, channelId);
const displayName = channel.pipe(
switchMap((c) => of$(c?.displayName)),
distinctUntilChanged(),
);
const channelIsDMorGM = channel.pipe(
switchMap((chan) => of$(chan ? isDMorGM(chan) : false)),
distinctUntilChanged(),
);
const callsState = observeCallsState(serverUrl);
const participants = callsState.pipe(
switchMap((state) => of$(state.calls[channelId])),
@@ -47,7 +53,7 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
);
const currentCallChannelName = currentCallChannelId.pipe(
switchMap((id) => observeChannel(database, id || '')),
switchMap((channel) => of$(channel ? channel.displayName : '')),
switchMap((c) => of$(c ? c.displayName : '')),
distinctUntilChanged(),
);
const channelCallStartTime = callsState.pipe(
@@ -57,6 +63,7 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
return {
displayName,
channelIsDMorGM,
participants,
inACall,
currentCallChannelName,

View File

@@ -5,8 +5,7 @@ import React from 'react';
import {useIntl} from 'react-intl';
import {View, Pressable} from 'react-native';
import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
@@ -23,6 +22,7 @@ type Props = {
channelId: string;
serverUrl: string;
displayName: string;
channelIsDMorGM: boolean;
inACall: boolean;
participants: UserModel[];
currentCallChannelName: string;
@@ -87,6 +87,7 @@ const JoinCallBanner = ({
channelId,
serverUrl,
displayName,
channelIsDMorGM,
participants,
inACall,
currentCallChannelName,
@@ -103,7 +104,7 @@ const JoinCallBanner = ({
showLimitRestrictedAlert(limitRestrictedInfo.maxParticipants, intl);
return;
}
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, inACall, false);
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, inACall, false, channelIsDMorGM);
};
return (

View File

@@ -1,79 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {hasMicrophonePermission, joinCall} from '@calls/actions';
import {errorAlert} from '@calls/utils';
import type {IntlShape} from 'react-intl';
export default function leaveAndJoinWithAlert(
intl: IntlShape,
serverUrl: string,
channelId: string,
leaveChannelName: string,
joinChannelName: string,
confirmToJoin: boolean,
newCall: boolean,
) {
if (confirmToJoin) {
const {formatMessage} = intl;
let joinMessage = formatMessage({
id: 'mobile.leave_and_join_message',
defaultMessage: 'You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and join the call in ~{joinChannelName}?',
}, {leaveChannelName, joinChannelName});
if (newCall) {
joinMessage = formatMessage({
id: 'mobile.leave_and_join_message',
defaultMessage: 'You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and start a new call in ~{joinChannelName}?',
}, {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',
}),
},
{
text: formatMessage({
id: 'mobile.leave_and_join_confirmation',
defaultMessage: 'Leave & Join',
}),
onPress: () => doJoinCall(serverUrl, channelId, intl),
style: 'cancel',
},
],
);
} else {
doJoinCall(serverUrl, channelId, intl);
}
}
export const doJoinCall = async (serverUrl: string, channelId: string, intl: IntlShape) => {
const {formatMessage} = intl;
const hasPermission = await hasMicrophonePermission(intl);
if (!hasPermission) {
errorAlert(formatMessage({
id: 'mobile.calls_error_permissions',
defaultMessage: 'No permissions to microphone, unable to start call',
}), intl);
return;
}
const res = await joinCall(serverUrl, channelId);
if (res.error) {
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
errorAlert(res.error?.toString() || seeLogs, intl);
}
};

View File

@@ -23,7 +23,7 @@ import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
import type {CallsConnection} from '@calls/types/calls';
const websocketConnectTimeout = 3000;
const peerConnectTimeout = 5000;
export async function newConnection(serverUrl: string, channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
let peer: Peer | null = null;
@@ -236,36 +236,36 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
}
});
const waitForReady = () => {
const waitForPeerConnection = () => {
const waitForReadyImpl = (callback: () => void, fail: () => void, timeout: number) => {
if (timeout <= 0) {
fail();
return;
}
setTimeout(() => {
if (ws.state() === WebSocket.OPEN) {
if (peer?.isConnected) {
callback();
} else {
waitForReadyImpl(callback, fail, timeout - 10);
waitForReadyImpl(callback, fail, timeout - 200);
}
}, 10);
}, 200);
};
const promise = new Promise<void>((resolve, reject) => {
waitForReadyImpl(resolve, reject, websocketConnectTimeout);
waitForReadyImpl(resolve, reject, peerConnectTimeout);
});
return promise;
};
const connection = {
const connection: CallsConnection = {
disconnect,
mute,
unmute,
waitForReady,
waitForPeerConnection,
raiseHand,
unraiseHand,
} as CallsConnection;
};
return connection;
}

View File

@@ -86,7 +86,7 @@ export type CallsConnection = {
disconnect: () => void;
mute: () => void;
unmute: () => void;
waitForReady: () => Promise<void>;
waitForPeerConnection: () => Promise<void>;
raiseHand: () => void;
unraiseHand: () => void;
}