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:
Christopher Poile 2022-04-06 14:51:40 -04:00 committed by GitHub
parent dbb6ceea04
commit 368584a1b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 88 additions and 15 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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