forked from Ivasoft/mattermost-mobile
Compare commits
4 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b42271960 | ||
|
|
1e4e59b537 | ||
|
|
fe29459906 | ||
|
|
8985791f40 |
@@ -131,8 +131,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 398
|
||||
versionName "1.52.0"
|
||||
versionCode 404
|
||||
versionName "1.53.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -27,6 +27,7 @@ import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {batchLoadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {
|
||||
handleCallStarted,
|
||||
handleCallEnded,
|
||||
handleCallUserConnected,
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
@@ -473,6 +474,8 @@ function handleEvent(msg: WebSocketMessage) {
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
return dispatch(handleCallStarted(msg));
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
return dispatch(handleCallEnded(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
return dispatch(handleCallScreenOn(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class DraftInput extends PureComponent {
|
||||
channelMemberCountsByGroup: PropTypes.object,
|
||||
groupsWithAllowReference: PropTypes.object,
|
||||
addRecentUsedEmojisInMessage: PropTypes.func.isRequired,
|
||||
endCallAlert: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -296,6 +297,12 @@ export default class DraftInput extends PureComponent {
|
||||
const {intl} = this.context;
|
||||
const {channelId, executeCommand, rootId, userIsOutOfOffice, theme} = this.props;
|
||||
|
||||
if (msg.trim() === '/call end') {
|
||||
this.props.endCallAlert(channelId);
|
||||
|
||||
// NOTE: fallthrough because the server may want to handle the command as well
|
||||
}
|
||||
|
||||
const status = DraftUtils.getStatusFromSlashCommand(msg);
|
||||
if (userIsOutOfOffice && DraftUtils.isStatusSlashCommand(status)) {
|
||||
confirmOutOfOfficeDisabled(intl, status, this.updateStatus);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {getAssociatedGroupsForReferenceMap} from '@mm-redux/selectors/entities/g
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
|
||||
import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {endCallAlert} from '@mmproducts/calls/store/actions/calls';
|
||||
import {isLandscape} from '@selectors/device';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from '@selectors/views';
|
||||
|
||||
@@ -103,6 +104,7 @@ const mapDispatchToProps = {
|
||||
setStatus,
|
||||
getChannelMemberCountsByGroup,
|
||||
addRecentUsedEmojisInMessage,
|
||||
endCallAlert,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostDraft);
|
||||
|
||||
@@ -12,4 +12,8 @@ const RequiredServer = {
|
||||
|
||||
const PluginId = 'com.mattermost.calls';
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis, PluginId};
|
||||
// Used for case when cloud server is using Calls v0.5.3.
|
||||
// This can be removed when v0.5.4 is prepackaged in cloud.
|
||||
const DefaultCloudMaxParticipants = 8;
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis, PluginId, DefaultCloudMaxParticipants};
|
||||
|
||||
@@ -65,6 +65,7 @@ const WebsocketEvents = {
|
||||
CALLS_USER_VOICE_ON: `custom_${Calls.PluginId}_user_voice_on`,
|
||||
CALLS_USER_VOICE_OFF: `custom_${Calls.PluginId}_user_voice_off`,
|
||||
CALLS_CALL_START: `custom_${Calls.PluginId}_call_start`,
|
||||
CALLS_CALL_END: `custom_${Calls.PluginId}_call_end`,
|
||||
CALLS_SCREEN_ON: `custom_${Calls.PluginId}_user_screen_on`,
|
||||
CALLS_SCREEN_OFF: `custom_${Calls.PluginId}_user_screen_off`,
|
||||
CALLS_USER_RAISE_HAND: `custom_${Calls.PluginId}_user_raise_hand`,
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ClientCallsMix {
|
||||
getCallsConfig: () => Promise<ServerConfig>;
|
||||
enableChannelCalls: (channelId: string) => Promise<ServerChannelState>;
|
||||
disableChannelCalls: (channelId: string) => Promise<ServerChannelState>;
|
||||
endCall: (channelId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientCalls = (superclass: any) => class extends superclass {
|
||||
@@ -51,6 +52,13 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
||||
{method: 'post', body: JSON.stringify({enabled: false})},
|
||||
);
|
||||
};
|
||||
|
||||
endCall = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/calls/${channelId}/end`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientCalls;
|
||||
|
||||
@@ -57,31 +57,40 @@ exports[`CallMessage should match snapshot 1`] = `
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-outline"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Join call
|
||||
@@ -239,31 +248,40 @@ exports[`CallMessage should match snapshot for the call already in the current c
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-outline"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Current call
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useCallback} from 'react';
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape, IntlShape} from 'react-intl';
|
||||
import {View, TouchableOpacity, Text} from 'react-native';
|
||||
|
||||
@@ -32,6 +32,7 @@ type CallMessageProps = {
|
||||
currentChannelName: string;
|
||||
callChannelName: string;
|
||||
intl: typeof IntlShape;
|
||||
isLimitRestricted: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -66,10 +67,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
joinCallButtonText: {
|
||||
color: 'white',
|
||||
},
|
||||
joinCallButtonTextRestricted: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
},
|
||||
joinCallButtonIcon: {
|
||||
color: 'white',
|
||||
marginRight: 5,
|
||||
},
|
||||
joinCallButtonIconRestricted: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.32),
|
||||
},
|
||||
startedText: {
|
||||
color: theme.centerChannelColor,
|
||||
fontWeight: 'bold',
|
||||
@@ -82,6 +89,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
},
|
||||
joinCallButtonRestricted: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
timeText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
@@ -98,14 +108,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInTheCall, theme, actions, userTimezone, isMilitaryTime, currentChannelName, callChannelName, intl}: CallMessageProps) => {
|
||||
const CallMessage = ({
|
||||
post,
|
||||
user,
|
||||
teammateNameDisplay,
|
||||
confirmToJoin,
|
||||
alreadyInTheCall,
|
||||
theme,
|
||||
actions,
|
||||
userTimezone,
|
||||
isMilitaryTime,
|
||||
currentChannelName,
|
||||
callChannelName,
|
||||
intl,
|
||||
isLimitRestricted,
|
||||
}: CallMessageProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const joinHandler = useCallback(() => {
|
||||
if (alreadyInTheCall) {
|
||||
const joinHandler = () => {
|
||||
if (alreadyInTheCall || isLimitRestricted) {
|
||||
return;
|
||||
}
|
||||
leaveAndJoinWithAlert(intl, post.channel_id, callChannelName, currentChannelName, confirmToJoin, actions.joinCall);
|
||||
}, [post.channel_id, callChannelName, currentChannelName, confirmToJoin, actions.joinCall]);
|
||||
};
|
||||
|
||||
if (post.props.end_at) {
|
||||
return (
|
||||
@@ -142,6 +166,8 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
|
||||
);
|
||||
}
|
||||
|
||||
const joinCallButtonText = alreadyInTheCall ? 'Current call' : 'Join call';
|
||||
|
||||
return (
|
||||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
@@ -161,22 +187,17 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={style.joinCallButton}
|
||||
style={[style.joinCallButton, isLimitRestricted && style.joinCallButtonRestricted]}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-outline'
|
||||
size={16}
|
||||
style={style.joinCallButtonIcon}
|
||||
style={[style.joinCallButtonIcon, isLimitRestricted && style.joinCallButtonIconRestricted]}
|
||||
/>
|
||||
{alreadyInTheCall &&
|
||||
<Text
|
||||
style={style.joinCallButtonText}
|
||||
>{'Current call'}</Text>}
|
||||
{!alreadyInTheCall &&
|
||||
<Text
|
||||
style={style.joinCallButtonText}
|
||||
>{'Join call'}</Text>}
|
||||
<Text style={[style.joinCallButtonText, isLimitRestricted && style.joinCallButtonTextRestricted]}>
|
||||
{joinCallButtonText}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getUser, getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
|
||||
import {joinCall} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import CallMessage from './call_message';
|
||||
|
||||
@@ -41,6 +41,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
userTimezone: enableTimezone ? getUserCurrentTimezone(currentUser.timezone) : undefined,
|
||||
currentChannelName: getChannel(state, post.channel_id)?.display_name,
|
||||
callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '',
|
||||
isLimitRestricted: isLimitRestricted(state, post.channel_id),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {GlobalState} from '@mm-redux/types/store';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
|
||||
import {useTryCallsFunction} from '@mmproducts/calls/hooks';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
|
||||
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
|
||||
import Separator from '@screens/channel_info/separator';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -28,6 +28,7 @@ const StartCall = ({testID, theme, intl, joinCall}: Props) => {
|
||||
const currentCall = useSelector(getCurrentCall);
|
||||
const currentCallChannelId = currentCall?.channelId || '';
|
||||
const callChannelName = useSelector((state: GlobalState) => getChannel(state, currentCallChannelId)?.display_name) || '';
|
||||
const limitRestricted = useSelector(isLimitRestricted);
|
||||
|
||||
const confirmToJoin = Boolean(currentCall && currentCall.channelId !== currentChannel.id);
|
||||
const alreadyInTheCall = Boolean(currentCall && call && currentCall.channelId === call.channelId);
|
||||
@@ -39,7 +40,7 @@ const StartCall = ({testID, theme, intl, joinCall}: Props) => {
|
||||
const [tryLeaveAndJoin, msgPostfix] = useTryCallsFunction(leaveAndJoin);
|
||||
const handleStartCall = useCallback(preventDoubleTap(tryLeaveAndJoin), [tryLeaveAndJoin]);
|
||||
|
||||
if (alreadyInTheCall) {
|
||||
if (alreadyInTheCall || limitRestricted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +1,100 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JoinCall should match snapshot 1`] = `
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#3DB887",
|
||||
"flexDirection": "row",
|
||||
"height": 38,
|
||||
"justifyContent": "center",
|
||||
"padding": 5,
|
||||
"width": "100%",
|
||||
"backgroundColor": "#ffffff",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#3DB887",
|
||||
"flexDirection": "row",
|
||||
"height": 38,
|
||||
"justifyContent": "center",
|
||||
"padding": 5,
|
||||
"width": "100%",
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
Join Call
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"fontWeight": "400",
|
||||
"marginLeft": 10,
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedRelativeTime
|
||||
updateIntervalInSeconds={1}
|
||||
value={100}
|
||||
/>
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginRight": 5,
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(Avatars)
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"paddingHorizontal": 16,
|
||||
"paddingVertical": 0,
|
||||
"top": 16,
|
||||
>
|
||||
Join Call
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"fontWeight": "400",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedRelativeTime
|
||||
updateIntervalInSeconds={1}
|
||||
value={100}
|
||||
/>
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(Avatars)
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"paddingHorizontal": 16,
|
||||
"paddingVertical": 0,
|
||||
"top": 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
Call participants
|
||||
</Text>
|
||||
}
|
||||
userIds={
|
||||
Array [
|
||||
"user-1-id",
|
||||
"user-2-id",
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
>
|
||||
Call participants
|
||||
</Text>
|
||||
}
|
||||
userIds={
|
||||
Array [
|
||||
"user-1-id",
|
||||
"user-2-id",
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {bindActionCreators, Dispatch} from 'redux';
|
||||
import {getChannel, getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {joinCall} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import JoinCall from './join_call';
|
||||
|
||||
@@ -23,6 +23,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
alreadyInTheCall: Boolean(currentCall && call && currentCall.channelId === call.channelId),
|
||||
currentChannelName: getChannel(state, currentChannelId)?.display_name,
|
||||
callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '',
|
||||
isLimitRestricted: isLimitRestricted(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('JoinCall', () => {
|
||||
alreadyInTheCall: false,
|
||||
currentChannelName: 'Current Channel',
|
||||
callChannelName: 'Call Channel',
|
||||
isLimitRestricted: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -56,7 +57,7 @@ describe('JoinCall', () => {
|
||||
test('should join on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive().childAt(0);
|
||||
|
||||
wrapper.simulate('press');
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
@@ -66,7 +67,7 @@ describe('JoinCall', () => {
|
||||
test('should ask for confirmation on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, confirmToJoin: true, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive().childAt(0);
|
||||
|
||||
wrapper.simulate('press');
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {View, Text, Pressable} from 'react-native';
|
||||
|
||||
@@ -26,49 +26,62 @@ type Props = {
|
||||
alreadyInTheCall: boolean;
|
||||
currentChannelName: string;
|
||||
callChannelName: string;
|
||||
isLimitRestricted: boolean;
|
||||
intl: typeof IntlShape;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3DB887',
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: JOIN_CALL_BAR_HEIGHT,
|
||||
},
|
||||
joinCallIcon: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: 10,
|
||||
marginRight: 5,
|
||||
},
|
||||
joinCall: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
},
|
||||
started: {
|
||||
flex: 1,
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '400',
|
||||
marginLeft: 10,
|
||||
},
|
||||
avatars: {
|
||||
marginRight: 5,
|
||||
},
|
||||
headerText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 0,
|
||||
top: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
outerContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
innerContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3DB887',
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: JOIN_CALL_BAR_HEIGHT,
|
||||
},
|
||||
innerContainerRestricted: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.48),
|
||||
},
|
||||
joinCallIcon: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: 10,
|
||||
marginRight: 5,
|
||||
},
|
||||
joinCall: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
},
|
||||
started: {
|
||||
flex: 1,
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '400',
|
||||
marginLeft: 10,
|
||||
},
|
||||
limitReached: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
textAlign: 'right',
|
||||
marginRight: 10,
|
||||
color: '#FFFFFFD6',
|
||||
fontWeight: '400',
|
||||
},
|
||||
avatars: {
|
||||
marginRight: 5,
|
||||
},
|
||||
headerText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 0,
|
||||
top: 16,
|
||||
},
|
||||
}));
|
||||
|
||||
const JoinCall = (props: Props) => {
|
||||
if (!props.call) {
|
||||
@@ -82,9 +95,12 @@ const JoinCall = (props: Props) => {
|
||||
};
|
||||
}, [props.call, props.alreadyInTheCall]);
|
||||
|
||||
const joinHandler = useCallback(() => {
|
||||
const joinHandler = () => {
|
||||
if (props.isLimitRestricted) {
|
||||
return;
|
||||
}
|
||||
leaveAndJoinWithAlert(props.intl, props.call.channelId, props.callChannelName, props.currentChannelName, props.confirmToJoin, props.actions.joinCall);
|
||||
}, [props.call.channelId, props.callChannelName, props.currentChannelName, props.confirmToJoin, props.actions.joinCall]);
|
||||
};
|
||||
|
||||
if (props.alreadyInTheCall) {
|
||||
return null;
|
||||
@@ -96,32 +112,39 @@ const JoinCall = (props: Props) => {
|
||||
}, [props.call.participants]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={style.container}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<Text style={style.joinCall}>{'Join Call'}</Text>
|
||||
<Text style={style.started}>
|
||||
<FormattedRelativeTime
|
||||
value={props.call.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
<View style={style.outerContainer}>
|
||||
<Pressable
|
||||
style={[style.innerContainer, props.isLimitRestricted && style.innerContainerRestricted]}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
</Text>
|
||||
<View style={style.avatars}>
|
||||
<Avatars
|
||||
userIds={userIds}
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<Text style={style.headerText}>{'Call participants'}</Text>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
<Text style={style.joinCall}>{'Join Call'}</Text>
|
||||
{props.isLimitRestricted ?
|
||||
<Text style={style.limitReached}>
|
||||
{'Participant limit reached'}
|
||||
</Text> :
|
||||
<Text style={style.started}>
|
||||
<FormattedRelativeTime
|
||||
value={props.call.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
<View style={style.avatars}>
|
||||
<Avatars
|
||||
userIds={userIds}
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<Text style={style.headerText}>{'Call participants'}</Text>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default injectIntl(JoinCall);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import {deflate} from 'pako/lib/deflate.js';
|
||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
import {
|
||||
MediaStream,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
} from 'react-native-webrtc';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import Peer from './simple-peer';
|
||||
import WebSocketClient from './websocket';
|
||||
@@ -20,12 +22,13 @@ export let client: any = null;
|
||||
|
||||
const websocketConnectTimeout = 3000;
|
||||
|
||||
export async function newClient(channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
|
||||
export async function newClient(channelID: string, iceServers: string[], closeCb: () => void, setScreenShareURL: (url: string) => void) {
|
||||
let peer: Peer | null = null;
|
||||
let stream: MediaStream;
|
||||
let voiceTrackAdded = false;
|
||||
let voiceTrack: MediaStreamTrack | null = null;
|
||||
let isClosed = false;
|
||||
let onCallEnd: EmitterSubscription | null = null;
|
||||
const streams: MediaStream[] = [];
|
||||
|
||||
try {
|
||||
@@ -47,6 +50,11 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
|
||||
ws.close();
|
||||
}
|
||||
|
||||
if (onCallEnd) {
|
||||
onCallEnd.remove();
|
||||
onCallEnd = null;
|
||||
}
|
||||
|
||||
streams.forEach((s) => {
|
||||
s.getTracks().forEach((track: MediaStreamTrack) => {
|
||||
track.stop();
|
||||
@@ -65,6 +73,12 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
|
||||
}
|
||||
};
|
||||
|
||||
onCallEnd = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_CALL_END, ({channelId}) => {
|
||||
if (channelId === channelID) {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
const mute = () => {
|
||||
if (!peer) {
|
||||
return;
|
||||
@@ -119,16 +133,8 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
|
||||
});
|
||||
|
||||
ws.on('join', async () => {
|
||||
let config;
|
||||
try {
|
||||
config = await Client4.getCallsConfig();
|
||||
} catch (err) {
|
||||
console.log('ERROR FETCHING CALLS CONFIG:', err); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
InCallManager.start({media: 'audio'});
|
||||
peer = new Peer(null, config.ICEServers);
|
||||
peer = new Peer(null, iceServers);
|
||||
peer.on('signal', (data: any) => {
|
||||
if (data.type === 'offer' || data.type === 'answer') {
|
||||
ws.send('sdp', {
|
||||
|
||||
@@ -323,6 +323,16 @@ const CallScreen = (props: Props) => {
|
||||
setShowControlsInLandscape(!showControlsInLandscape);
|
||||
}, [showControlsInLandscape]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_CALL_END, ({channelId}) => {
|
||||
if (channelId === props.call?.channelId) {
|
||||
popTopScreen();
|
||||
}
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
if (!props.call) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import keyMirror from '@mm-redux/utils/key_mirror';
|
||||
export default keyMirror({
|
||||
RECEIVED_CALLS: null,
|
||||
RECEIVED_CALL_STARTED: null,
|
||||
RECEIVED_CALL_ENDED: null,
|
||||
RECEIVED_CALL_FINISHED: null,
|
||||
RECEIVED_CHANNEL_CALL_ENABLED: null,
|
||||
RECEIVED_CHANNEL_CALL_DISABLED: null,
|
||||
|
||||
@@ -117,11 +117,14 @@ describe('Actions.Calls', () => {
|
||||
expect(CallsActions.ws.disconnect).not.toBeCalled();
|
||||
const disconnectMock = CallsActions.ws.disconnect;
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
|
||||
// ws.disconnect calls the callback, which is what sends the CallsTypes.RECEIVED_MYSELF_LEFT_CALL action.
|
||||
expect(disconnectMock).toBeCalled();
|
||||
await store.dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
|
||||
expect(CallsActions.ws).toBe(null);
|
||||
|
||||
result = store.getState().entities.calls.joined;
|
||||
assert.equal('', result);
|
||||
assert.equal(result, '');
|
||||
});
|
||||
|
||||
it('muteMyself', async () => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
import {batch} from 'react-redux';
|
||||
|
||||
@@ -9,6 +10,10 @@ import {Client4} from '@client/rest';
|
||||
import Calls from '@constants/calls';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getCurrentUserRoles, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {
|
||||
GenericAction,
|
||||
ActionFunc,
|
||||
@@ -17,10 +22,16 @@ import {
|
||||
ActionResult,
|
||||
} from '@mm-redux/types/actions';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import {displayUsername, isAdmin as checkIsAdmin} from '@mm-redux/utils/user_utils';
|
||||
import {newClient} from '@mmproducts/calls/connection';
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
import {getConfig} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {
|
||||
getCallInCurrentChannel,
|
||||
getConfig,
|
||||
getNumCurrentConnectedParticipants,
|
||||
} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {Call, CallParticipant, DefaultServerConfig} from '@mmproducts/calls/store/types/calls';
|
||||
import {getUserIdFromDM} from '@mmproducts/calls/utils';
|
||||
import {hasMicrophonePermission} from '@utils/permission';
|
||||
|
||||
export let ws: any = null;
|
||||
@@ -82,6 +93,7 @@ export function loadCalls(): ActionFunc {
|
||||
speakers: [],
|
||||
screenOn: channel.call.screen_sharing_id,
|
||||
threadId: channel.call.thread_id,
|
||||
creatorId: channel.call.creator_id,
|
||||
};
|
||||
}
|
||||
enabledChannels[channel.channel_id] = channel.enabled;
|
||||
@@ -189,7 +201,10 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc
|
||||
dispatch(setSpeakerphoneOn(false));
|
||||
|
||||
try {
|
||||
ws = await newClient(channelId, () => null, setScreenShareURL);
|
||||
ws = await newClient(channelId, getConfig(getState()).ICEServers, () => {
|
||||
dispatch(setSpeakerphoneOn(false));
|
||||
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
|
||||
}, setScreenShareURL);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
@@ -212,13 +227,11 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc
|
||||
}
|
||||
|
||||
export function leaveCall(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
return async () => {
|
||||
if (ws) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
dispatch(setSpeakerphoneOn(false));
|
||||
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
|
||||
return {};
|
||||
};
|
||||
}
|
||||
@@ -260,3 +273,51 @@ export function setSpeakerphoneOn(newState: boolean): GenericAction {
|
||||
data: newState,
|
||||
};
|
||||
}
|
||||
|
||||
export function endCallAlert(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const userId = getCurrentUserId(getState());
|
||||
const numParticipants = getNumCurrentConnectedParticipants(getState());
|
||||
const channel = getCurrentChannel(getState());
|
||||
const currentCall = getCallInCurrentChannel(getState());
|
||||
const roles = getCurrentUserRoles(getState());
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
|
||||
if (!isAdmin && userId !== currentCall?.creatorId) {
|
||||
Alert.alert('Error', 'You do not have permission to end the call. Please ask the call creator to end call.');
|
||||
return {};
|
||||
}
|
||||
|
||||
let msg = `Are you sure you want to end a call with ${numParticipants} participants in ${channel.display_name}?`;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const otherID = getUserIdFromDM(channel.name, getCurrentUserId(getState()));
|
||||
const otherUser = getUser(getState(), otherID);
|
||||
const nameDisplay = getTeammateNameDisplaySetting(getState());
|
||||
msg = `Are you sure you want to end a call with ${displayUsername(otherUser, nameDisplay)}?`;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'End call',
|
||||
msg,
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'End call',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await Client4.endCall(channelId);
|
||||
} catch (e) {
|
||||
const err = e.message || 'unable to complete command, see server logs';
|
||||
Alert.alert('Error', `Error: ${err}`);
|
||||
}
|
||||
},
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +50,15 @@ export function handleCallUserVoiceOff(msg: WebSocketMessage) {
|
||||
export function handleCallStarted(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CALL_STARTED,
|
||||
data: {channelId: msg.data.channelID, startTime: msg.data.start_at, threadId: msg.data.thread_id, participants: {}},
|
||||
data: {channelId: msg.data.channelID, startTime: msg.data.start_at, threadId: msg.data.thread_id, participants: {}, creatorId: msg.data.creator_id},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallEnded(msg: WebSocketMessage): GenericAction {
|
||||
DeviceEventEmitter.emit(WebsocketEvents.CALLS_CALL_END, {channelId: msg.broadcast.channel_id});
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CALL_ENDED,
|
||||
data: {channelId: msg.broadcast.channel_id},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,16 @@ describe('Reducers.calls.calls', () => {
|
||||
assert.deepEqual(state.calls, {'channel-2': call2});
|
||||
});
|
||||
|
||||
it('RECEIVED_CALL_ENDED', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALL_FINISHED,
|
||||
data: {channelId: 'channel-1'},
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-2': call2});
|
||||
});
|
||||
|
||||
it('RECEIVED_MUTE_USER_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
|
||||
@@ -53,6 +53,11 @@ function calls(state: Dictionary<Call> = {}, action: GenericAction) {
|
||||
nextState[newCall.channelId] = newCall;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALL_ENDED: {
|
||||
const nextState = {...state};
|
||||
delete nextState[action.data.channelId];
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALL_FINISHED: {
|
||||
const newCall = action.data;
|
||||
const nextState = {...state};
|
||||
@@ -162,6 +167,12 @@ function joined(state = '', action: GenericAction) {
|
||||
case CallsTypes.RECEIVED_MYSELF_JOINED_CALL: {
|
||||
return action.data;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALL_ENDED: {
|
||||
if (action.data.channelId === state) {
|
||||
return '';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case CallsTypes.RECEIVED_MYSELF_LEFT_CALL: {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze';
|
||||
import {DefaultServerConfig} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
import * as Selectors from './calls';
|
||||
|
||||
describe('Selectors.Calls', () => {
|
||||
const call1 = {id: 'call1'};
|
||||
const call1 = {id: 'call1', participants: [{id: 'me'}]};
|
||||
const call2 = {id: 'call2'};
|
||||
const testState = deepFreezeAndThrowOnMutation({
|
||||
entities: {
|
||||
@@ -20,6 +21,10 @@ describe('Selectors.Calls', () => {
|
||||
joined: 'call1',
|
||||
enabled: {'channel-1': true, 'channel-2': false},
|
||||
screenShareURL: 'screenshare-url',
|
||||
config: DefaultServerConfig,
|
||||
},
|
||||
general: {
|
||||
license: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -77,4 +82,119 @@ describe('Selectors.Calls', () => {
|
||||
it('getScreenShareURL', () => {
|
||||
assert.equal(Selectors.getScreenShareURL(testState), 'screenshare-url');
|
||||
});
|
||||
|
||||
it('isLimitRestricted', () => {
|
||||
// Default, no limit
|
||||
assert.equal(Selectors.isLimitRestricted(testState, 'call1'), false);
|
||||
|
||||
let newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
config: {
|
||||
...testState.entities.calls.config,
|
||||
MaxCallParticipants: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Limit to 1 and one participant already in call.
|
||||
assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true);
|
||||
|
||||
// Limit to 1 but no call ongoing.
|
||||
assert.equal(Selectors.isLimitRestricted(newState), false);
|
||||
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
general: {
|
||||
license: {Cloud: 'true'},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// On cloud, no limit.
|
||||
assert.equal(Selectors.isLimitRestricted(newState, 'call1'), false);
|
||||
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
config: {
|
||||
...testState.entities.calls.config,
|
||||
MaxCallParticipants: 1,
|
||||
},
|
||||
},
|
||||
general: {
|
||||
license: {Cloud: 'true'},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// On cloud, with limit.
|
||||
assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true);
|
||||
|
||||
const call = {id: 'call1',
|
||||
participants: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
]};
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
calls: {call1: call},
|
||||
},
|
||||
general: {
|
||||
license: {Cloud: 'true'},
|
||||
},
|
||||
},
|
||||
};
|
||||
delete newState.entities.calls.config.MaxCallParticipants;
|
||||
|
||||
// On cloud, MaxCallParticipants missing, default should be used.
|
||||
assert.equal(Selectors.isLimitRestricted(newState, 'call1'), false);
|
||||
|
||||
const newCall = {id: 'call1',
|
||||
participants: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
]};
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
calls: {call1: newCall},
|
||||
},
|
||||
general: {
|
||||
license: {Cloud: 'true'},
|
||||
},
|
||||
},
|
||||
};
|
||||
delete newState.entities.calls.config.MaxCallParticipants;
|
||||
|
||||
// On cloud, MaxCallParticipants missing, default should be used.
|
||||
assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import Calls from '@constants/calls';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
|
||||
import {getServerVersion} from '@mm-redux/selectors/entities/general';
|
||||
import {getLicense, getServerVersion} from '@mm-redux/selectors/entities/general';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {Call} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
export function getConfig(state: GlobalState) {
|
||||
return state.entities.calls.config;
|
||||
@@ -65,3 +68,49 @@ export function isSupportedServer(state: GlobalState) {
|
||||
export function isCallsPluginEnabled(state: GlobalState) {
|
||||
return state.entities.calls.pluginEnabled;
|
||||
}
|
||||
|
||||
export const getCallInCurrentChannel: (state: GlobalState) => Call | undefined = createSelector(
|
||||
getCurrentChannelId,
|
||||
getCalls,
|
||||
(currentChannelId, calls) => calls[currentChannelId],
|
||||
);
|
||||
|
||||
export const getNumCurrentConnectedParticipants: (state: GlobalState) => number = createSelector(
|
||||
getCurrentChannelId,
|
||||
getCalls,
|
||||
(currentChannelId, calls) => {
|
||||
const participants = calls[currentChannelId]?.participants;
|
||||
if (!participants) {
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(participants).length || 0;
|
||||
},
|
||||
);
|
||||
|
||||
const isCloud: (state: GlobalState) => boolean = createSelector(
|
||||
getLicense,
|
||||
(license) => license?.Cloud === 'true',
|
||||
);
|
||||
|
||||
export const isLimitRestricted: (state: GlobalState, channelId?: string) => boolean = createSelector(
|
||||
isCloud,
|
||||
(state: GlobalState, channelId: string) => (channelId ? getCalls(state)[channelId] : getCallInCurrentChannel(state)),
|
||||
getConfig,
|
||||
(cloud, call, config) => {
|
||||
if (!call) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const numParticipants = Object.keys(call.participants || {}).length;
|
||||
|
||||
// TODO: The next block is used for case when cloud server is using Calls v0.5.3. This can be removed
|
||||
// when v0.5.4 is prepackaged in cloud. Then replace the max in the return statement with
|
||||
// config.MaxCallParticipants.
|
||||
let max = config.MaxCallParticipants;
|
||||
if (cloud && !max) {
|
||||
max = Calls.DefaultCloudMaxParticipants;
|
||||
}
|
||||
|
||||
return max !== 0 && numParticipants >= max;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,12 +15,13 @@ export type CallsState = {
|
||||
}
|
||||
|
||||
export type Call = {
|
||||
participants: Dictionary<CallParticipant>;
|
||||
participants: Dictionary<CallParticipant>;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
speakers: string[];
|
||||
screenOn: string;
|
||||
threadId: string;
|
||||
creatorId: string;
|
||||
}
|
||||
|
||||
export type CallParticipant = {
|
||||
@@ -49,6 +50,7 @@ export type ServerCallState = {
|
||||
states: ServerUserState[];
|
||||
thread_id: string;
|
||||
screen_sharing_id: string;
|
||||
creator_id: string;
|
||||
}
|
||||
|
||||
export type VoiceEventData = {
|
||||
@@ -60,6 +62,8 @@ export type ServerConfig = {
|
||||
ICEServers: string[];
|
||||
AllowEnableCalls: boolean;
|
||||
DefaultEnabled: boolean;
|
||||
MaxCallParticipants: number;
|
||||
sku_short_name: string;
|
||||
last_retrieved_at: number;
|
||||
}
|
||||
|
||||
@@ -67,5 +71,7 @@ export const DefaultServerConfig = {
|
||||
ICEServers: [],
|
||||
AllowEnableCalls: false,
|
||||
DefaultEnabled: false,
|
||||
MaxCallParticipants: 0,
|
||||
sku_short_name: '',
|
||||
last_retrieved_at: 0,
|
||||
} as ServerConfig;
|
||||
|
||||
@@ -48,3 +48,14 @@ const sortByState = (presenterID?: string) => {
|
||||
return 0;
|
||||
};
|
||||
};
|
||||
|
||||
export function getUserIdFromDM(dmName: string, currentUserId: string) {
|
||||
const ids = dmName.split('__');
|
||||
let otherUserId = '';
|
||||
if (ids[0] === currentUserId) {
|
||||
otherUserId = ids[1];
|
||||
} else {
|
||||
otherUserId = ids[0];
|
||||
}
|
||||
return otherUserId;
|
||||
}
|
||||
|
||||
@@ -911,7 +911,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 404;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -953,7 +953,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 398;
|
||||
CURRENT_PROJECT_VERSION = 404;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.52.0</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -37,7 +37,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>398</string>
|
||||
<string>404</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.52.0</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>398</string>
|
||||
<string>404</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.52.0</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>398</string>
|
||||
<string>404</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -733,7 +733,7 @@ SPEC CHECKSUMS:
|
||||
FBLazyVector: 244195e30d63d7f564c55da4410b9a24e8fbceaa
|
||||
FBReactNativeSpec: c94002c1d93da3658f4d5119c6994d19961e3d52
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: 5337263514dd6f09803962437687240c5dc39aa4
|
||||
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
|
||||
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
|
||||
iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba
|
||||
jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073
|
||||
@@ -746,7 +746,7 @@ SPEC CHECKSUMS:
|
||||
Permission-Notifications: bb420c3d28328df24de1b476b41ed8249ccf2537
|
||||
Permission-PhotoLibrary: 7bec836dcdd04a0bfb200c314f1aae06d4476357
|
||||
Permission-PhotoLibraryAddOnly: 06fb0cdb1d35683b235ad8c464ef0ecc88859ea3
|
||||
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
|
||||
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
|
||||
RCTRequired: cd47794163052d2b8318c891a7a14fcfaccc75ab
|
||||
RCTTypeSafety: 393bb40b3e357b224cde53d3fec26813c52428b1
|
||||
RCTYouTube: a8bb45705622a6fc9decf64be04128d3658ed411
|
||||
@@ -824,4 +824,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 8214414d5676358401d8ad51dff19e7fd8c71b5c
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "1.52.0",
|
||||
"version": "1.53.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "1.52.0",
|
||||
"version": "1.53.0",
|
||||
"description": "Mattermost Mobile with React Native",
|
||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||
"author": "Mattermost, Inc.",
|
||||
|
||||
Reference in New Issue
Block a user