forked from Ivasoft/mattermost-mobile
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:
committed by
GitHub
parent
9ab4c935ef
commit
38b68733d0
@@ -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()),
|
||||
})),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export type CallsConnection = {
|
||||
disconnect: () => void;
|
||||
mute: () => void;
|
||||
unmute: () => void;
|
||||
waitForReady: () => Promise<void>;
|
||||
waitForPeerConnection: () => Promise<void>;
|
||||
raiseHand: () => void;
|
||||
unraiseHand: () => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user