forked from Ivasoft/mattermost-mobile
Compare commits
22 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b42271960 | ||
|
|
1e4e59b537 | ||
|
|
fe29459906 | ||
|
|
8985791f40 | ||
|
|
7d9d7d45ac | ||
|
|
738d25ec3b | ||
|
|
ffe1ef2494 | ||
|
|
79e9f043f3 | ||
|
|
9aef24070d | ||
|
|
fef4cfa93d | ||
|
|
54a243f578 | ||
|
|
24607f3d42 | ||
|
|
61370801ab | ||
|
|
edc682b02a | ||
|
|
9b4bc2d6a2 | ||
|
|
c020222c1f | ||
|
|
d5856f7b06 | ||
|
|
67c65156a7 | ||
|
|
cd34873c23 | ||
|
|
912287fbe0 | ||
|
|
c1e3c878e3 | ||
|
|
fe5bd60cec |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
"about.teamEditiont1": "Корпоративна версия",
|
||||
"about.title": "Относно {appTitle}",
|
||||
"announcment_banner.dont_show_again": "Не показвай отново",
|
||||
"api.channel.add_guest.added": "{addedUsername} е добавен към канала като гост от {username}.",
|
||||
"api.channel.add_member.added": "{addedUsername} е добавен в канала от {username}.",
|
||||
"api.channel.guest_join_channel.post_and_forget": "{username} се присъедини към канала като гост.",
|
||||
"apps.error": "Грешка: {error}",
|
||||
"apps.error.command.field_missing": "Липсват задължителни полета: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "Повтаря се каналът с поле `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_channel": "Неизвестен канал за полето `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_option": "Неизвестна опция за полето `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_user": "Неизвестен потребител за поле `{fieldName}`: `{option}`.",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"apps.error.form.refresh_no_refresh": "Frissítést hívott egy frissítés nélküli mezőn.",
|
||||
"apps.error.form.submit.pretext": "Hiba történt az adatok elküldése során. Lépjen kapcsolatba az alkalmazás fejlesztőjével. Részletek: {details}",
|
||||
"apps.error.lookup.error_preparing_request": "Hiba a keresési kérelem előkészítése során: {errorMessage}",
|
||||
"apps.error.malformed_binding": "Ez az összekapcsolás nincs jól formázva. Lépjen kapcsolatba az App fejlesztőjével.",
|
||||
"apps.error.malformed_binding": "Ez a kapcsolódás nincs jól formázva. Lépjen kapcsolatba az App fejlesztőjével.",
|
||||
"apps.error.parser": "Feldolgozási hiba: {error}",
|
||||
"apps.error.parser.empty_value": "üres értékek nem megengedettek",
|
||||
"apps.error.parser.execute_non_leaf": "Ki kell választania egy alparancsot.",
|
||||
@@ -39,7 +39,7 @@
|
||||
"apps.error.parser.missing_list_end": "Az elvárt a lista záró token.",
|
||||
"apps.error.parser.missing_quote": "Egyező idézőjel elvárt a bemenet vége előtt.",
|
||||
"apps.error.parser.missing_source": "Az űrlapnak nincs sem submit sem source mezője.",
|
||||
"apps.error.parser.missing_submit": "Nincs submit meghívás az összekapcsolásban vagy a formon.",
|
||||
"apps.error.parser.missing_submit": "Nincs submit meghívás az összekötőben vagy az űrlapban.",
|
||||
"apps.error.parser.missing_tick": "Azonos idézőjelel kell használni a bemenet vége előtt.",
|
||||
"apps.error.parser.multiple_equal": "Többszörös `=` jel nem engedélyezett.",
|
||||
"apps.error.parser.no_argument_pos_x": "Nem sikerült azonosítani az argumentumot.",
|
||||
|
||||
@@ -16,35 +16,49 @@
|
||||
"api.channel.guest_join_channel.post_and_forget": "{username}이(가) 게스트로 채널에 참여했습니다.",
|
||||
"apps.error": "오류: {error}",
|
||||
"apps.error.command.field_missing": "필수 필드 누락: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "필드 `{fieldName}`에서 채널이 반복 지정되었습니다: `{option}`.",
|
||||
"apps.error.command.same_option": "필드 `{fieldName}` 에서 옵션이 반복 지정되었습니다: `{option}`.",
|
||||
"apps.error.command.same_user": "필드 `{fieldName}` 에서 사용자가 반복 지정되었습니다: `{option}`.",
|
||||
"apps.error.command.unknown_channel": "`{fieldName}` 필드에 대한 알 수 없는 채널: `{option}`.",
|
||||
"apps.error.command.unknown_option": "`{fieldName}` 필드에 대한 알 수 없는 옵션: `{option}`.",
|
||||
"apps.error.command.unknown_user": "`{fieldName}` 필드의 알 수 없는 사용자: `{option}`.",
|
||||
"apps.error.form.no_form": "`form`이 정의되어 있지 않습니다.",
|
||||
"apps.error.form.no_lookup": "`lookup` 이 정의되어 있지 않습니다.",
|
||||
"apps.error.form.no_source": "`source` 가 정의되어 있지 않습니다.",
|
||||
"apps.error.form.no_submit": "`submit` 가 정의되어 있지 않습니다",
|
||||
"apps.error.form.refresh": "선택한 필드를 가져오는 동안 오류가 발생했습니다. 앱 개발자에게 문의하세요. 세부정보: {details}",
|
||||
"apps.error.form.refresh_no_refresh": "새로고침이 불가능한 필드에 대해 새로고침을 실행합니다.",
|
||||
"apps.error.form.submit.pretext": "모달을 제출하는 중에 오류가 발생했습니다. 앱 개발자에게 문의하세요. 세부정보: {details}",
|
||||
"apps.error.lookup.error_preparing_request": "조회 요청을 준비하는 중 오류 발생: {errorMessage}",
|
||||
"apps.error.malformed_binding": "바인딩이 제대로 형성되지 않았습니다. 앱 개발자에게 문의하십시오.",
|
||||
"apps.error.parser": "파싱 오류: {error}",
|
||||
"apps.error.parser.empty_value": "빈 값은 허용되지 않습니다",
|
||||
"apps.error.parser.execute_non_leaf": "하위 명령을 선택해야 합니다.",
|
||||
"apps.error.parser.missing_binding": "커맨드 연결을 찾을 수 없습니다.",
|
||||
"apps.error.parser.missing_field_value": "필드 값이 누락되었습니다.",
|
||||
"apps.error.parser.missing_list_end": "목록을 닫는 토큰이 필요합니다.",
|
||||
"apps.error.parser.missing_quote": "입력 종료 전에 \"가 필요합니다.",
|
||||
"apps.error.parser.missing_source": "양식이 제출되지 않았거나 원본이 없습니다.",
|
||||
"apps.error.parser.missing_submit": "바인딩 또는 양식에 제출 호출이 없습니다.",
|
||||
"apps.error.parser.missing_tick": "입력 종료 전에 '가 필요합니다.",
|
||||
"apps.error.parser.multiple_equal": "'=' 기호는 여러 개 사용할 수 없습니다.",
|
||||
"apps.error.parser.no_argument_pos_x": "인수를 식별할 수 없습니다.",
|
||||
"apps.error.parser.no_bindings": "바인딩 된 명령어가 없습니다.",
|
||||
"apps.error.parser.no_form": "양식을 찾을 수 없습니다.",
|
||||
"apps.error.parser.no_match": "`{command}`: 이 작업 공간에서 일치하는 명령어를 찾을 수 없습니다.",
|
||||
"apps.error.parser.no_slash_start": "명령어는 '/'로 시작해야 합니다.",
|
||||
"apps.error.parser.unexpected_character": "예기치 않은 문자.",
|
||||
"apps.error.parser.unexpected_comma": "예기치 않은 콤마.",
|
||||
"apps.error.parser.unexpected_error": "예기치 않은 오류입니다.",
|
||||
"apps.error.parser.unexpected_flag": "명령어에서 '{flagName}' 플래그를 허용하지 않습니다.",
|
||||
"apps.error.parser.unexpected_state": "연결할 수 없음: matchBinding에서 예기치 않은 상태: '{state}'.",
|
||||
"apps.error.parser.unexpected_flag": "명령어에서`{flagName}`플래그를 허용하지 않습니다.",
|
||||
"apps.error.parser.unexpected_squared_bracket": "예기치 않은 목록이 열립니다.",
|
||||
"apps.error.parser.unexpected_state": "연결할 수 없음: matchBinding에서 예기치 않은 상태: `{state}`.",
|
||||
"apps.error.parser.unexpected_whitespace": "연결할 수 없음: 예기치 않은 공백입니다.",
|
||||
"apps.error.responses.form.no_form": "응답 유형은 `form`이지만 응답에 `form`이 포함되지 않았습니다.",
|
||||
"apps.error.responses.navigate.no_url": "응답 유형은 `navigate`이지만, URL이 포함되지 않았습니다.",
|
||||
"apps.error.responses.unexpected_error": "예기치 않은 오류를 수신했습니다.",
|
||||
"apps.error.responses.unexpected_type": "예상하지 못한 앱 응답 유형입니다. 응답 유형: {type}.",
|
||||
"apps.error.responses.unknown_field_error": "알 수 없는 필드에 대한 오류를 수신했습니다. 필드 이름: '{field}'. 오류: '{error}'.",
|
||||
"apps.error.responses.unknown_field_error": "알 수 없는 필드에 대한 오류를 수신했습니다. 필드 이름: `{field}`. 오류: `{error}`.",
|
||||
"apps.error.responses.unknown_type": "지원되지 않는 앱 응답 유형입니다. 응답 유형: {type}.",
|
||||
"apps.error.unknown": "알 수 없는 오류가 발생했습니다.",
|
||||
"apps.suggestion.dynamic.error": "동적 선택 오류",
|
||||
@@ -401,6 +415,8 @@
|
||||
"mobile.message_length.message": "Your current message is too long. Current character count: {max}/{count}",
|
||||
"mobile.message_length.message_split_left": "메시지가 문자 제한을 초과합니다",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.microphone_permission_denied_description": "이 호출에 참여하려면 마이크로폰에 대한 가장 중요한 액세스 권한을 부여하기 위한 설정을 여십시오.",
|
||||
"mobile.microphone_permission_denied_title": "{applicationName}이(가) 마이크에 액세스하려고 합니다",
|
||||
"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",
|
||||
|
||||
@@ -686,7 +686,7 @@
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "werd niet verwittigd door deze vermelding omdat deze niet in het kanaal zijn. Zij kunnen niet aan het kanaal toegevoegd worden omdat ze geen lid zijn van de gekoppelde groepen. Om hem aan het kanaal toe te voegen, moeten ze toegevoegd worden aan de gelinkte groepen.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " en ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "voeg ze toe aan dit privé-kanaal",
|
||||
"post_body.check_for_out_of_channel_mentions.link.public": "voeg ze toe aan het kanaal",
|
||||
"post_body.check_for_out_of_channel_mentions.link.public": "ze toevoegen aan het kanaal",
|
||||
"post_body.check_for_out_of_channel_mentions.message.multiple": "niet door deze vermelding op de hoogte zijn gebracht omdat ze niet in het kanaal aanwezig zijn. Wil je ",
|
||||
"post_body.check_for_out_of_channel_mentions.message.one": "niet door deze vermelding op de hoogte zijn gebracht omdat ze niet in het kanaal aanwezig zijn. Wil je ",
|
||||
"post_body.check_for_out_of_channel_mentions.message_last": "? Ze zullen toegang hebben tot de volledige berichtgeschiedenis.",
|
||||
|
||||
@@ -415,6 +415,8 @@
|
||||
"mobile.message_length.message": "Ditt meddelande är för långt. Nuvarande antal tecken: {max}/{count}",
|
||||
"mobile.message_length.message_split_left": "Meddelandet överskrider teckenbegränsningen",
|
||||
"mobile.message_length.title": "Meddelandelängd",
|
||||
"mobile.microphone_permission_denied_description": "För att delta i samtalet, öppna Inställningar och ge Mattermost tillåtelse att använda mikrofonen.",
|
||||
"mobile.microphone_permission_denied_title": "{applicationName} behöver åtkomst till din mikrofon",
|
||||
"mobile.more_dms.add_more": "Du kan lägga till {remaining, number} användare",
|
||||
"mobile.more_dms.cannot_add_more": "Du kan lägga till 1 till användare",
|
||||
"mobile.more_dms.one_more": "Du kan lägga till 1 till användare",
|
||||
@@ -701,7 +703,7 @@
|
||||
"rhs_thread.rootPostDeletedMessage.body": "Delar av denna tråd har raderats utifrån gallringsregler. Det går inte att svara i denna tråd.",
|
||||
"search_bar.search": "Sök",
|
||||
"search_header.results": "Sökresultat",
|
||||
"search_header.title2": "Nyliga omnämnanden",
|
||||
"search_header.title2": "Senaste omnämnanden",
|
||||
"search_header.title3": "Sparade meddelanden",
|
||||
"search_item.channelArchived": "Arkiverad",
|
||||
"sidebar.channels": "PUBLIKA KANALER",
|
||||
|
||||
@@ -415,6 +415,8 @@
|
||||
"mobile.message_length.message": "您的消息过长。目前字数:{max}/{count}",
|
||||
"mobile.message_length.message_split_left": "消息超过字数限制",
|
||||
"mobile.message_length.title": "消息长度",
|
||||
"mobile.microphone_permission_denied_description": "若要参与此呼叫,请打开“设置”以授予对您的麦克风“授权使用”的权限。",
|
||||
"mobile.microphone_permission_denied_title": "{applicationName} 想使用您的麦克风",
|
||||
"mobile.more_dms.add_more": "您还可以添加 {remaining, number} 位用户",
|
||||
"mobile.more_dms.cannot_add_more": "您不能再添加更多用户",
|
||||
"mobile.more_dms.one_more": "您还能添加 1 个用户",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"about.title": "關於 {appTitle}",
|
||||
"announcment_banner.dont_show_again": "不要再顯示",
|
||||
"api.channel.add_member.added": "{username} 已將 {addedUsername} 加入頻道",
|
||||
"apps.error": "錯誤:{error}",
|
||||
"archivedChannelMessage": "正在觀看**被封存的頻道**。不能發布新訊息。",
|
||||
"center_panel.archived.closeChannel": "關閉頻道",
|
||||
"channel.channelHasGuests": "此頻道有訪客",
|
||||
|
||||
@@ -8,16 +8,16 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.581.0)
|
||||
aws-sdk-core (3.130.2)
|
||||
aws-partitions (1.590.0)
|
||||
aws-sdk-core (3.131.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.56.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.113.2)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
@@ -66,7 +66,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.205.2)
|
||||
fastlane (2.206.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -111,9 +111,9 @@ GEM
|
||||
fastlane-plugin-find_replace_string (0.1.0)
|
||||
fastlane-plugin-versioning_android (0.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.19.0)
|
||||
google-apis-androidpublisher_v3 (0.21.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.4.2)
|
||||
google-apis-core (0.5.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -126,7 +126,7 @@ GEM
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.13.0)
|
||||
google-apis-storage_v1 (0.14.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
@@ -154,7 +154,7 @@ GEM
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.3.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
@@ -164,7 +164,7 @@ GEM
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
nokogiri (1.13.4)
|
||||
nokogiri (1.13.6)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
optparse (0.1.1)
|
||||
@@ -173,7 +173,7 @@ GEM
|
||||
public_suffix (4.0.7)
|
||||
racc (1.6.0)
|
||||
rake (13.0.6)
|
||||
representable (3.1.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
|
||||
@@ -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