forked from Ivasoft/mattermost-mobile
MM-49970 - Calls: Call quality status banner (#7304)
* use mean opinion score detection; add call quality alert bar; tests * rename MessageBar's file to message_bar * i18n * use mosThreshold from calls-common; upgrade calls-common * use callsBg for text and icon
This commit is contained in:
committed by
GitHub
parent
f8f6839945
commit
c90ace706c
@@ -14,7 +14,21 @@ const RequiredServer = {
|
||||
|
||||
const PluginId = 'com.mattermost.calls';
|
||||
|
||||
export const REACTION_TIMEOUT = 10000;
|
||||
export const REACTION_LIMIT = 20;
|
||||
const REACTION_TIMEOUT = 10000;
|
||||
const REACTION_LIMIT = 20;
|
||||
const CALL_QUALITY_RESET_MS = toMilliseconds({minutes: 1});
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis, PluginId, REACTION_TIMEOUT};
|
||||
export enum MessageBarType {
|
||||
Microphone,
|
||||
CallQuality,
|
||||
}
|
||||
|
||||
export default {
|
||||
RefreshConfigMillis,
|
||||
RequiredServer,
|
||||
PluginId,
|
||||
REACTION_TIMEOUT,
|
||||
REACTION_LIMIT,
|
||||
MessageBarType,
|
||||
CALL_QUALITY_RESET_MS,
|
||||
};
|
||||
|
||||
@@ -9,12 +9,13 @@ import {leaveCall, muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import MessageBar from '@calls/components/message_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {setCallQualityAlertDismissed, setMicPermissionsErrorDismissed} from '@calls/state';
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {Calls, Screens} from '@constants';
|
||||
import {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {allOrientations, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
@@ -273,7 +274,18 @@ const CurrentCallBar = ({
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
{micPermissionsError &&
|
||||
<MessageBar
|
||||
type={Calls.MessageBarType.Microphone}
|
||||
onPress={setMicPermissionsErrorDismissed}
|
||||
/>
|
||||
}
|
||||
{currentCall?.callQualityAlert &&
|
||||
<MessageBar
|
||||
type={Calls.MessageBarType.CallQuality}
|
||||
onPress={setCallQualityAlertDismissed}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
152
app/products/calls/components/message_bar.tsx
Normal file
152
app/products/calls/components/message_bar.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Pressable, Text, View} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {makeCallsTheme} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Calls} from '@constants';
|
||||
import {CALL_ERROR_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {MessageBarType} from '@app/constants/calls';
|
||||
import type {CallsTheme} from '@calls/types/calls';
|
||||
|
||||
type Props = {
|
||||
type: MessageBarType;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => (
|
||||
{
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
errorWrapper: {
|
||||
padding: 10,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
minHeight: CALL_ERROR_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
warningBar: {
|
||||
backgroundColor: theme.awayIndicator,
|
||||
},
|
||||
errorText: {
|
||||
flex: 1,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
warningText: {
|
||||
color: theme.callsBg,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
margin: 0,
|
||||
padding: 9,
|
||||
},
|
||||
pressedIconContainer: {
|
||||
backgroundColor: theme.buttonColor,
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 18,
|
||||
},
|
||||
warningIcon: {
|
||||
color: theme.callsBg,
|
||||
},
|
||||
pressedErrorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
pressedWarningIcon: {
|
||||
color: theme.awayIndicator,
|
||||
},
|
||||
paddingRight: {
|
||||
paddingRight: 9,
|
||||
},
|
||||
}
|
||||
));
|
||||
|
||||
const MessageBar = ({type, onPress}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
|
||||
const style = getStyleSheet(callsTheme);
|
||||
const warning = type === Calls.MessageBarType.CallQuality;
|
||||
|
||||
let message = '';
|
||||
let icon = <></>;
|
||||
switch (type) {
|
||||
case Calls.MessageBarType.Microphone:
|
||||
message = intl.formatMessage({
|
||||
id: 'mobile.calls_mic_error',
|
||||
defaultMessage: 'To participate, open Settings to grant Mattermost access to your microphone.',
|
||||
});
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='microphone-off'
|
||||
style={[style.errorIcon, style.paddingRight]}
|
||||
/>);
|
||||
break;
|
||||
case Calls.MessageBarType.CallQuality:
|
||||
message = intl.formatMessage({
|
||||
id: 'mobile.calls_quality_warning',
|
||||
defaultMessage: 'Call quality may be degraded due to unstable network conditions.',
|
||||
});
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='alert-outline'
|
||||
style={[style.errorIcon, style.warningIcon, style.paddingRight]}
|
||||
/>);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.errorWrapper}>
|
||||
<Pressable
|
||||
onPress={Permissions.openSettings}
|
||||
style={[style.errorBar, warning && style.warningBar]}
|
||||
>
|
||||
{icon}
|
||||
<Text style={[style.errorText, warning && style.warningText]}>{message}</Text>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
hitSlop={5}
|
||||
style={({pressed}) => [
|
||||
style.pressable,
|
||||
style.iconContainer,
|
||||
pressed && style.pressedIconContainer,
|
||||
]}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<CompassIcon
|
||||
name='close'
|
||||
style={[style.errorIcon,
|
||||
warning && style.warningIcon,
|
||||
pressed && style.pressedErrorIcon,
|
||||
pressed && warning && style.pressedWarningIcon,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageBar;
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {setMicPermissionsErrorDismissed} from '@calls/state';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CALL_ERROR_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => (
|
||||
{
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
errorWrapper: {
|
||||
padding: 10,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
minHeight: CALL_ERROR_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorText: {
|
||||
flex: 1,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
errorIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
margin: 0,
|
||||
padding: 9,
|
||||
},
|
||||
pressedErrorIconContainer: {
|
||||
backgroundColor: theme.buttonColor,
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 18,
|
||||
},
|
||||
pressedErrorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
paddingRight: {
|
||||
paddingRight: 9,
|
||||
},
|
||||
}
|
||||
));
|
||||
|
||||
const PermissionErrorBar = () => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.errorWrapper}>
|
||||
<Pressable
|
||||
onPress={Permissions.openSettings}
|
||||
style={style.errorBar}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone-off'
|
||||
style={[style.errorIcon, style.paddingRight]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_mic_error'}
|
||||
defaultMessage={'To participate, open Settings to grant Mattermost access to your microphone.'}
|
||||
style={style.errorText}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={setMicPermissionsErrorDismissed}
|
||||
hitSlop={5}
|
||||
style={({pressed}) => [
|
||||
style.pressable,
|
||||
style.errorIconContainer,
|
||||
pressed && style.pressedErrorIconContainer,
|
||||
]}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<CompassIcon
|
||||
name='close'
|
||||
style={[style.errorIcon, pressed && style.pressedErrorIcon]}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionErrorBar;
|
||||
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RTCPeer} from '@mattermost/calls/lib';
|
||||
import {RTCMonitor, RTCPeer} from '@mattermost/calls/lib';
|
||||
import {deflate} from 'pako';
|
||||
import {DeviceEventEmitter, type EmitterSubscription, Platform} from 'react-native';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
import {mediaDevices, MediaStream, MediaStreamTrack, RTCPeerConnection} from 'react-native-webrtc';
|
||||
|
||||
import {setPreferredAudioRoute, setSpeakerphoneOn} from '@calls/actions/calls';
|
||||
import {setAudioDeviceInfo} from '@calls/state';
|
||||
import {processMeanOpinionScore, setAudioDeviceInfo} from '@calls/state';
|
||||
import {AudioDevice, type AudioDeviceInfo, type AudioDeviceInfoRaw, type CallsConnection} from '@calls/types/calls';
|
||||
import {getICEServersConfigs} from '@calls/utils';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
@@ -22,6 +22,7 @@ import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
|
||||
import type {EmojiData} from '@mattermost/calls/lib/types';
|
||||
|
||||
const peerConnectTimeout = 5000;
|
||||
const rtcMonitorInterval = 4000;
|
||||
|
||||
export async function newConnection(
|
||||
serverUrl: string,
|
||||
@@ -39,6 +40,13 @@ export async function newConnection(
|
||||
let onCallEnd: EmitterSubscription | null = null;
|
||||
let audioDeviceChanged: EmitterSubscription | null = null;
|
||||
const streams: MediaStream[] = [];
|
||||
let rtcMonitor: RTCMonitor | null = null;
|
||||
const logger = {
|
||||
logDebug,
|
||||
logErr: logError,
|
||||
logWarn: logWarning,
|
||||
logInfo,
|
||||
};
|
||||
|
||||
const initializeVoiceTrack = async () => {
|
||||
if (voiceTrack) {
|
||||
@@ -79,6 +87,7 @@ export async function newConnection(
|
||||
|
||||
ws.send('leave');
|
||||
ws.close();
|
||||
rtcMonitor?.stop();
|
||||
|
||||
if (onCallEnd) {
|
||||
onCallEnd.remove();
|
||||
@@ -133,6 +142,14 @@ export async function newConnection(
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: we purposely clear the monitor's stats cache upon unmuting
|
||||
// in order to skip some calculations since upon muting we actually
|
||||
// stop sending packets which would result in stats to be skewed as
|
||||
// soon as we resume sending.
|
||||
// This is not perfect but it avoids having to constantly send
|
||||
// silence frames when muted.
|
||||
rtcMonitor?.clearCache();
|
||||
|
||||
try {
|
||||
if (voiceTrackAdded) {
|
||||
peer.replaceTrack(voiceTrack.id, voiceTrack);
|
||||
@@ -233,18 +250,20 @@ export async function newConnection(
|
||||
|
||||
peer = new RTCPeer({
|
||||
iceServers: iceConfigs || [],
|
||||
logger: {
|
||||
logDebug,
|
||||
logErr: logError,
|
||||
logWarn: logWarning,
|
||||
logInfo,
|
||||
},
|
||||
logger,
|
||||
webrtc: {
|
||||
MediaStream,
|
||||
RTCPeerConnection,
|
||||
},
|
||||
});
|
||||
|
||||
rtcMonitor = new RTCMonitor({
|
||||
peer,
|
||||
logger,
|
||||
monitorInterval: rtcMonitorInterval,
|
||||
});
|
||||
rtcMonitor.on('mos', processMeanOpinionScore);
|
||||
|
||||
peer.on('offer', (sdp) => {
|
||||
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);
|
||||
ws.send('sdp', {
|
||||
@@ -323,6 +342,7 @@ export async function newConnection(
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (peer?.connected) {
|
||||
rtcMonitor?.start();
|
||||
callback();
|
||||
} else {
|
||||
waitForReadyImpl(callback, fail, timeout - 200);
|
||||
|
||||
@@ -30,17 +30,17 @@ import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
|
||||
import EmojiList from '@calls/components/emoji_list';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import MessageBar from '@calls/components/message_bar';
|
||||
import ReactionBar from '@calls/components/reaction_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {RaisedHandBanner} from '@calls/screens/call_screen/raised_hand_banner';
|
||||
import {useCallsConfig} from '@calls/state';
|
||||
import {setCallQualityAlertDismissed, setMicPermissionsErrorDismissed, useCallsConfig} from '@calls/state';
|
||||
import {getHandsRaised, makeCallsTheme, sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import {Preferences, Screens, WebsocketEvents} from '@constants';
|
||||
import {Calls, Preferences, Screens, WebsocketEvents} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -694,7 +694,18 @@ const CallScreen = ({
|
||||
{!isLandscape && currentCall.reactionStream.length > 0 &&
|
||||
<EmojiList reactionStream={currentCall.reactionStream}/>
|
||||
}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
{micPermissionsError &&
|
||||
<MessageBar
|
||||
type={Calls.MessageBarType.Microphone}
|
||||
onPress={setMicPermissionsErrorDismissed}
|
||||
/>
|
||||
}
|
||||
{currentCall.callQualityAlert &&
|
||||
<MessageBar
|
||||
type={Calls.MessageBarType.CallQuality}
|
||||
onPress={setCallQualityAlertDismissed}
|
||||
/>
|
||||
}
|
||||
<View style={[style.buttonsContainer]}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -7,7 +7,10 @@ import {act, renderHook} from '@testing-library/react-hooks';
|
||||
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
newCurrentCall, setAudioDeviceInfo,
|
||||
newCurrentCall,
|
||||
processMeanOpinionScore,
|
||||
setAudioDeviceInfo,
|
||||
setCallQualityAlertDismissed,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
@@ -58,6 +61,10 @@ import type {CallRecordingState} from '@mattermost/calls/lib/types';
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
|
||||
jest.mock('@constants/calls', () => ({
|
||||
CALL_QUALITY_RESET_MS: 100,
|
||||
}));
|
||||
|
||||
jest.mock('@actions/remote/thread', () => ({
|
||||
updateThreadFollowing: jest.fn(() => Promise.resolve({})),
|
||||
}));
|
||||
@@ -897,6 +904,83 @@ describe('useCallsState', () => {
|
||||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('CallQuality', async () => {
|
||||
const initialCallsState: CallsState = {
|
||||
...DefaultCallsState,
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const newCall1: Call = {
|
||||
...call1,
|
||||
participants: {
|
||||
...call1.participants,
|
||||
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
|
||||
},
|
||||
};
|
||||
const expectedCallsState: CallsState = {
|
||||
...initialCallsState,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
const currentCallNoAlertNoDismissed: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
connected: true,
|
||||
...newCall1,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
act(() => setCallsState('server1', initialCallsState));
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], null);
|
||||
|
||||
// join call
|
||||
act(() => {
|
||||
newCurrentCall('server1', 'channel-1', 'myUserId');
|
||||
userJoinedCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], currentCallNoAlertNoDismissed);
|
||||
|
||||
// call quality goes bad
|
||||
act(() => processMeanOpinionScore(3.4999));
|
||||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true);
|
||||
assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed, 0);
|
||||
|
||||
// call quality goes good
|
||||
act(() => processMeanOpinionScore(4));
|
||||
assert.deepEqual(result.current[1], currentCallNoAlertNoDismissed);
|
||||
|
||||
// call quality goes bad
|
||||
act(() => processMeanOpinionScore(3.499));
|
||||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true);
|
||||
assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed, 0);
|
||||
|
||||
// dismiss call quality alert
|
||||
const timeNow = Date.now();
|
||||
act(() => setCallQualityAlertDismissed());
|
||||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, false);
|
||||
assert.equal((result.current[1] as CurrentCall).callQualityAlertDismissed >= timeNow &&
|
||||
(result.current[1] as CurrentCall).callQualityAlertDismissed <= Date.now(), true);
|
||||
|
||||
// call quality goes bad, but we're not past the dismissed limit
|
||||
act(() => processMeanOpinionScore(3.4999));
|
||||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, false);
|
||||
|
||||
// test that the dismiss expired
|
||||
await act(async () => {
|
||||
await new Promise((r) => setTimeout(r, 101));
|
||||
processMeanOpinionScore(3.499);
|
||||
});
|
||||
assert.deepEqual((result.current[1] as CurrentCall).callQualityAlert, true);
|
||||
});
|
||||
|
||||
it('voiceOn and Off', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {mosThreshold} from '@mattermost/calls/lib/rtc_monitor';
|
||||
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
DefaultCurrentCall,
|
||||
type ReactionStreamEmoji,
|
||||
} from '@calls/types/calls';
|
||||
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
|
||||
import {Calls} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getThreadById} from '@queries/servers/thread';
|
||||
@@ -489,7 +491,7 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: User
|
||||
};
|
||||
newReactionStream.splice(0, 0, newReaction);
|
||||
}
|
||||
if (newReactionStream.length > REACTION_LIMIT) {
|
||||
if (newReactionStream.length > Calls.REACTION_LIMIT) {
|
||||
newReactionStream.pop();
|
||||
}
|
||||
|
||||
@@ -509,7 +511,7 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: User
|
||||
|
||||
setTimeout(() => {
|
||||
userReactionTimeout(serverUrl, channelId, reaction);
|
||||
}, REACTION_TIMEOUT);
|
||||
}, Calls.REACTION_TIMEOUT);
|
||||
};
|
||||
|
||||
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
|
||||
@@ -591,3 +593,54 @@ export const setHost = (serverUrl: string, channelId: string, hostId: string) =>
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const processMeanOpinionScore = (mos: number) => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mos < mosThreshold) {
|
||||
setCallQualityAlert(true);
|
||||
} else {
|
||||
setCallQualityAlert(false);
|
||||
}
|
||||
};
|
||||
|
||||
export const setCallQualityAlert = (setAlert: boolean) => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alert is already active, or alert was dismissed and the timeout hasn't passed
|
||||
if ((setAlert && currentCall.callQualityAlert) ||
|
||||
(setAlert && currentCall.callQualityAlertDismissed + Calls.CALL_QUALITY_RESET_MS > Date.now())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alert is already inactive
|
||||
if ((!setAlert && !currentCall.callQualityAlert)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall: CurrentCall = {
|
||||
...currentCall,
|
||||
callQualityAlert: setAlert,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setCallQualityAlertDismissed = () => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall: CurrentCall = {
|
||||
...currentCall,
|
||||
callQualityAlert: false,
|
||||
callQualityAlertDismissed: Date.now(),
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,8 @@ export type CurrentCall = Call & {
|
||||
voiceOn: Dictionary<boolean>;
|
||||
micPermissionsErrorDismissed: boolean;
|
||||
reactionStream: ReactionStreamEmoji[];
|
||||
recAcknowledged: boolean;
|
||||
callQualityAlert: boolean;
|
||||
callQualityAlertDismissed: number;
|
||||
}
|
||||
|
||||
export const DefaultCurrentCall: CurrentCall = {
|
||||
@@ -77,7 +78,8 @@ export const DefaultCurrentCall: CurrentCall = {
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
reactionStream: [],
|
||||
recAcknowledged: false,
|
||||
callQualityAlert: false,
|
||||
callQualityAlertDismissed: 0,
|
||||
};
|
||||
|
||||
export type CallParticipant = {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls/lib/utils';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {Post} from '@constants';
|
||||
import Calls from '@constants/calls';
|
||||
import {Calls, Post} from '@constants';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
|
||||
@@ -466,6 +466,7 @@
|
||||
"mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.",
|
||||
"mobile.calls_participant_rec_title": "Recording is in progress",
|
||||
"mobile.calls_phone": "Phone",
|
||||
"mobile.calls_quality_warning": "Call quality may be degraded due to unstable network conditions.",
|
||||
"mobile.calls_raise_hand": "Raise hand",
|
||||
"mobile.calls_raised_hand": "<bold>{name} {num, plural, =0 {} other {+# more }}</bold>raised a hand",
|
||||
"mobile.calls_react": "React",
|
||||
|
||||
Reference in New Issue
Block a user