MM-42320: Calls - small fix for microphone permissions (#6134)
* small fix for microphone permissions * use standard permissions check/denied popups * tests and i18n * permission wording * 18n
This commit is contained in:
parent
dbb6ceea04
commit
368584a1b7
10 changed files with 88 additions and 15 deletions
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useCallback} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape, IntlShape} from 'react-intl';
|
||||
import {View, TouchableOpacity, Text} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -19,7 +19,7 @@ import type {UserProfile} from '@mm-redux/types/users';
|
|||
|
||||
type CallMessageProps = {
|
||||
actions: {
|
||||
joinCall: (channelId: string) => void;
|
||||
joinCall: (channelId: string, intl: typeof intlShape) => void;
|
||||
};
|
||||
post: Post;
|
||||
user: UserProfile;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import EnableDisableCalls from '@mmproducts/calls/components/channel_info/enable_disable_calls';
|
||||
|
|
@ -10,7 +11,7 @@ import {useCallsChannelSettings} from '@mmproducts/calls/hooks';
|
|||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
joinCall: (channelId: string) => void;
|
||||
joinCall: (channelId: string, intl: typeof intlShape) => void;
|
||||
}
|
||||
|
||||
const CallsChannelInfo = ({theme, joinCall}: Props) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape, IntlShape} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getChannel, getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
||||
|
|
@ -19,7 +19,7 @@ type Props = {
|
|||
testID: string;
|
||||
theme: Theme;
|
||||
intl: typeof IntlShape;
|
||||
joinCall: (channelId: string) => void;
|
||||
joinCall: (channelId: string, intl: typeof intlShape) => void;
|
||||
}
|
||||
|
||||
const StartCall = ({testID, theme, intl, joinCall}: Props) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type {Call} from '@mmproducts/calls/store/types/calls';
|
|||
|
||||
type Props = {
|
||||
actions: {
|
||||
joinCall: (channelId: string) => any;
|
||||
joinCall: (channelId: string, intl: typeof IntlShape) => void;
|
||||
};
|
||||
theme: Theme;
|
||||
call: Call;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
export default function leaveAndJoinWithAlert(intl: typeof IntlShape, channelId: string, callChannelName: string, currentChannelName: string, confirmToJoin: boolean, joinCall: (channelId: string) => void) {
|
||||
export default function leaveAndJoinWithAlert(intl: typeof IntlShape, channelId: string, callChannelName: string, currentChannelName: string, confirmToJoin: boolean, joinCall: (channelId: string, intl: typeof IntlShape) => void) {
|
||||
if (confirmToJoin) {
|
||||
Alert.alert(
|
||||
'Are you sure you want to switch to a different call?',
|
||||
|
|
@ -15,12 +15,12 @@ export default function leaveAndJoinWithAlert(intl: typeof IntlShape, channelId:
|
|||
},
|
||||
{
|
||||
text: 'Leave & Join',
|
||||
onPress: () => joinCall(channelId),
|
||||
onPress: () => joinCall(channelId, intl),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
joinCall(channelId);
|
||||
joinCall(channelId, intl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
|
||||
import assert from 'assert';
|
||||
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import configureStore from '@test/test_store';
|
||||
import * as PermissionUtils from '@utils/permission';
|
||||
|
||||
import CallsTypes from '../action_types/calls';
|
||||
|
||||
|
|
@ -73,6 +75,9 @@ describe('Actions.Calls', () => {
|
|||
let store;
|
||||
const {newClient} = require('@mmproducts/calls/connection');
|
||||
InCallManager.setSpeakerphoneOn = jest.fn();
|
||||
const intlProvider = new IntlProvider({locale: 'en'}, {});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
jest.spyOn(PermissionUtils, 'hasMicrophonePermission').mockReturnValue(true);
|
||||
|
||||
beforeEach(async () => {
|
||||
newClient.mockClear();
|
||||
|
|
@ -86,7 +91,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
it('joinCall', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
const response = await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
const response = await store.dispatch(CallsActions.joinCall('channel-id', intl));
|
||||
const result = store.getState().entities.calls.joined;
|
||||
assert.equal('channel-id', result);
|
||||
assert.equal(response.data, 'channel-id');
|
||||
|
|
@ -99,7 +104,7 @@ describe('Actions.Calls', () => {
|
|||
await store.dispatch(addFakeCall('channel-id'));
|
||||
expect(CallsActions.ws).toBe(null);
|
||||
|
||||
await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
await store.dispatch(CallsActions.joinCall('channel-id', intl));
|
||||
let result = store.getState().entities.calls.joined;
|
||||
assert.equal('channel-id', result);
|
||||
|
||||
|
|
@ -115,7 +120,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
it('muteMyself', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
await store.dispatch(CallsActions.joinCall('channel-id', intl));
|
||||
await store.dispatch(CallsActions.muteMyself());
|
||||
expect(CallsActions.ws.mute).toBeCalled();
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
|
|
@ -13,6 +14,7 @@ import {newClient} from '@mmproducts/calls/connection';
|
|||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
import {getConfig} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {Call, CallParticipant, DefaultServerConfig} from '@mmproducts/calls/store/types/calls';
|
||||
import {hasMicrophonePermission} from '@utils/permission';
|
||||
|
||||
export let ws: any = null;
|
||||
|
||||
|
|
@ -131,8 +133,13 @@ export function disableChannelCalls(channelId: string): ActionFunc {
|
|||
};
|
||||
}
|
||||
|
||||
export function joinCall(channelId: string): ActionFunc {
|
||||
export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const hasPermission = await hasMicrophonePermission(intl);
|
||||
if (!hasPermission) {
|
||||
return {error: 'no permissions to microphone, unable to start call'};
|
||||
}
|
||||
|
||||
const setScreenShareURL = (url: string) => {
|
||||
dispatch({
|
||||
type: CallsTypes.SET_SCREENSHARE_URL,
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export default class ChannelInfo extends PureComponent {
|
|||
dismissModal();
|
||||
};
|
||||
|
||||
joinCallHandler = (channelId) => {
|
||||
this.props.actions.joinCall(channelId);
|
||||
joinCallHandler = (channelId, intl) => {
|
||||
this.props.actions.joinCall(channelId, intl);
|
||||
this.close();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,21 @@ const getStoragePermissionDeniedMessage = (intl: typeof intlShape) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getMicrophonePermissionDeniedMessage = (intl: typeof intlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your microphone',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_description',
|
||||
defaultMessage: 'To participate in this call, open Settings to grant Mattermost access to your microphone.',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const hasCameraPermission = async (intl: typeof intlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const targetSource = Platform.OS === 'ios' ? Permissions.PERMISSIONS.IOS.CAMERA : Permissions.PERMISSIONS.ANDROID.CAMERA;
|
||||
|
|
@ -108,6 +123,49 @@ export const hasCameraPermission = async (intl: typeof intlShape) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
export const hasMicrophonePermission = async (intl: typeof intlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const targetSource = Platform.OS === 'ios' ? Permissions.PERMISSIONS.IOS.MICROPHONE : Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO;
|
||||
const hasPermission = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermission) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
case Permissions.RESULTS.UNAVAILABLE: {
|
||||
const permissionRequest = await Permissions.request(targetSource);
|
||||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = getMicrophonePermissionDeniedMessage(intl);
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const hasPhotoPermission = async (intl: typeof intlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const targetSource = Platform.OS === 'ios' ? Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY : Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
|
|
|
|||
|
|
@ -415,6 +415,8 @@
|
|||
"mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}",
|
||||
"mobile.message_length.message_split_left": "Message exceeds the character limit",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.microphone_permission_denied_description": "To participate in this call, open Settings to grant Mattermost access to your microphone.",
|
||||
"mobile.microphone_permission_denied_title": "{applicationName} would like to access your microphone",
|
||||
"mobile.more_dms.add_more": "You can add {remaining, number} more users",
|
||||
"mobile.more_dms.cannot_add_more": "You cannot add more users",
|
||||
"mobile.more_dms.one_more": "You can add 1 more user",
|
||||
|
|
|
|||
Loading…
Reference in a new issue