forked from Ivasoft/mattermost-mobile
Compare commits
22 Commits
v1.52.0
...
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 |
36
NOTICE.txt
36
NOTICE.txt
@@ -2273,6 +2273,42 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-math-view
|
||||
|
||||
This product contains 'react-native-math-view' by Shachar.
|
||||
|
||||
A react native view used to easily display and handle math. The library doesn't use WebView.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/ShaMan123/react-native-math-view
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 ShaMan123
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
© 2022 GitHub, Inc.
|
||||
|
||||
---
|
||||
|
||||
## react-native-mmkv-storage
|
||||
|
||||
This product contains 'react-native-mmkv-storage' by Ammar Ahmed.
|
||||
|
||||
@@ -131,8 +131,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 392
|
||||
versionName "1.51.1"
|
||||
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:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import {RNFetchBlobFetchRepsonse} from 'rn-fetch-blob';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import Calls from '@constants/calls';
|
||||
import {Options} from '@mm-redux/types/client4';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
@@ -286,12 +287,16 @@ export default class ClientBase {
|
||||
return `${this.getUserThreadsRoute(userId, teamId)}/${threadId}`;
|
||||
}
|
||||
|
||||
getPluginsRoute() {
|
||||
return `${this.getBaseRoute()}/plugins`;
|
||||
}
|
||||
|
||||
getAppsProxyRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.calls`;
|
||||
return `${this.url}/plugins/${Calls.PluginId}`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ClientPlugins, {ClientPluginsMix} from '@client/rest/plugins';
|
||||
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
|
||||
import mix from '@utils/mix';
|
||||
|
||||
@@ -36,7 +37,8 @@ interface Client extends ClientBase,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix,
|
||||
ClientCallsMix
|
||||
ClientCallsMix,
|
||||
ClientPluginsMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
@@ -55,6 +57,7 @@ class Client extends mix(ClientBase).with(
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
ClientPlugins,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
||||
19
app/client/rest/plugins.ts
Normal file
19
app/client/rest/plugins.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ClientPluginManifest} from '@mm-redux/types/plugins';
|
||||
|
||||
export interface ClientPluginsMix {
|
||||
getPluginsManifests: () => Promise<ClientPluginManifest[]>;
|
||||
}
|
||||
|
||||
const ClientPlugins = (superclass: any) => class extends superclass {
|
||||
getPluginsManifests = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPluginsRoute()}/webapp`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPlugins;
|
||||
@@ -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);
|
||||
|
||||
@@ -10,4 +10,10 @@ const RequiredServer = {
|
||||
PATCH_VERSION: 0,
|
||||
};
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis};
|
||||
const PluginId = 'com.mattermost.calls';
|
||||
|
||||
// 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};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Calls from '@constants/calls';
|
||||
|
||||
const WebsocketEvents = {
|
||||
POSTED: 'posted',
|
||||
POST_EDITED: 'post_edited',
|
||||
@@ -53,18 +56,19 @@ const WebsocketEvents = {
|
||||
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
|
||||
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',
|
||||
SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated',
|
||||
CALLS_CHANNEL_ENABLED: 'custom_com.mattermost.calls_channel_enable_voice',
|
||||
CALLS_CHANNEL_DISABLED: 'custom_com.mattermost.calls_channel_disable_voice',
|
||||
CALLS_USER_CONNECTED: 'custom_com.mattermost.calls_user_connected',
|
||||
CALLS_USER_DISCONNECTED: 'custom_com.mattermost.calls_user_disconnected',
|
||||
CALLS_USER_MUTED: 'custom_com.mattermost.calls_user_muted',
|
||||
CALLS_USER_UNMUTED: 'custom_com.mattermost.calls_user_unmuted',
|
||||
CALLS_USER_VOICE_ON: 'custom_com.mattermost.calls_user_voice_on',
|
||||
CALLS_USER_VOICE_OFF: 'custom_com.mattermost.calls_user_voice_off',
|
||||
CALLS_CALL_START: 'custom_com.mattermost.calls_call_start',
|
||||
CALLS_SCREEN_ON: 'custom_com.mattermost.calls_user_screen_on',
|
||||
CALLS_SCREEN_OFF: 'custom_com.mattermost.calls_user_screen_off',
|
||||
CALLS_USER_RAISE_HAND: 'custom_com.mattermost.calls_user_raise_hand',
|
||||
CALLS_USER_UNRAISE_HAND: 'custom_com.mattermost.calls_user_unraise_hand',
|
||||
CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`,
|
||||
CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`,
|
||||
CALLS_USER_CONNECTED: `custom_${Calls.PluginId}_user_connected`,
|
||||
CALLS_USER_DISCONNECTED: `custom_${Calls.PluginId}_user_disconnected`,
|
||||
CALLS_USER_MUTED: `custom_${Calls.PluginId}_user_muted`,
|
||||
CALLS_USER_UNMUTED: `custom_${Calls.PluginId}_user_unmuted`,
|
||||
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`,
|
||||
CALLS_USER_UNRAISE_HAND: `custom_${Calls.PluginId}_user_unraise_hand`,
|
||||
};
|
||||
export default WebsocketEvents;
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -11,7 +11,12 @@ import {getCurrentChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserRoles} from '@mm-redux/selectors/entities/users';
|
||||
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin} from '@mm-redux/utils/user_utils';
|
||||
import {loadConfig} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getConfig, isCallsExplicitlyDisabled, isCallsExplicitlyEnabled} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {
|
||||
getConfig,
|
||||
isCallsExplicitlyDisabled,
|
||||
isCallsExplicitlyEnabled,
|
||||
isCallsPluginEnabled,
|
||||
} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
// Check if calls is enabled. If it is, then run fn; if it isn't, show an alert and set
|
||||
// msgPostfix to ' (Not Available)'.
|
||||
@@ -45,12 +50,15 @@ export const useCallsChannelSettings = () => {
|
||||
const dispatch = useDispatch();
|
||||
const config = useSelector(getConfig);
|
||||
const currentChannel = useSelector(getCurrentChannel);
|
||||
const pluginEnabled = useSelector(isCallsPluginEnabled);
|
||||
const explicitlyDisabled = useSelector(isCallsExplicitlyDisabled);
|
||||
const explicitlyEnabled = useSelector(isCallsExplicitlyEnabled);
|
||||
const roles = useSelector(getCurrentUserRoles);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadConfig());
|
||||
if (pluginEnabled) {
|
||||
dispatch(loadConfig());
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
|
||||
@@ -58,9 +66,11 @@ export const useCallsChannelSettings = () => {
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isChannelAdmin = isAdmin || checkIsChannelAdmin(roles);
|
||||
|
||||
const enabled = (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled));
|
||||
const enabled = pluginEnabled && (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled));
|
||||
let canEnableDisable;
|
||||
if (config.AllowEnableCalls) {
|
||||
if (!pluginEnabled) {
|
||||
canEnableDisable = false;
|
||||
} else if (config.AllowEnableCalls) {
|
||||
canEnableDisable = isDirectMessage || isGroupMessage || isChannelAdmin;
|
||||
} else {
|
||||
canEnableDisable = isAdmin;
|
||||
|
||||
@@ -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,
|
||||
@@ -24,4 +25,5 @@ export default keyMirror({
|
||||
SET_SCREENSHARE_URL: null,
|
||||
SET_SPEAKERPHONE: null,
|
||||
RECEIVED_CONFIG: null,
|
||||
RECEIVED_PLUGIN_ENABLED: null,
|
||||
});
|
||||
|
||||
@@ -39,6 +39,12 @@ jest.mock('@client/rest', () => ({
|
||||
DefaultEnabled: true,
|
||||
last_retrieved_at: 1234,
|
||||
})),
|
||||
getPluginsManifests: jest.fn(() => (
|
||||
[
|
||||
{id: 'playbooks'},
|
||||
{id: 'com.mattermost.calls'},
|
||||
]
|
||||
)),
|
||||
enableChannelCalls: jest.fn(() => null),
|
||||
disableChannelCalls: jest.fn(() => null),
|
||||
},
|
||||
@@ -111,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 () => {
|
||||
@@ -135,14 +144,14 @@ describe('Actions.Calls', () => {
|
||||
});
|
||||
|
||||
it('loadCalls', async () => {
|
||||
await store.dispatch(await store.dispatch(CallsActions.loadCalls()));
|
||||
await store.dispatch(CallsActions.loadCalls());
|
||||
expect(Client4.getCalls).toBeCalledWith();
|
||||
assert.equal(store.getState().entities.calls.calls['channel-1'].channelId, 'channel-1');
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], true);
|
||||
});
|
||||
|
||||
it('loadConfig', async () => {
|
||||
await store.dispatch(await store.dispatch(CallsActions.loadConfig()));
|
||||
await store.dispatch(CallsActions.loadConfig());
|
||||
expect(Client4.getCallsConfig).toBeCalledWith();
|
||||
assert.equal(store.getState().entities.calls.config.DefaultEnabled, true);
|
||||
assert.equal(store.getState().entities.calls.config.AllowEnableCalls, true);
|
||||
@@ -150,6 +159,7 @@ describe('Actions.Calls', () => {
|
||||
|
||||
it('batchLoadConfig', async () => {
|
||||
await store.dispatch(CallsActions.batchLoadCalls());
|
||||
expect(Client4.getPluginsManifests).toBeCalledWith();
|
||||
expect(Client4.getCallsConfig).toBeCalledWith();
|
||||
expect(Client4.getCalls).toBeCalledWith();
|
||||
|
||||
|
||||
@@ -2,24 +2,42 @@
|
||||
// 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';
|
||||
|
||||
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 {GenericAction, ActionFunc, DispatchFunc, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
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,
|
||||
DispatchFunc,
|
||||
GetStateFunc,
|
||||
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;
|
||||
|
||||
export function loadConfig(force = false): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<GenericAction> => {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
if (!force) {
|
||||
if ((Date.now() - getConfig(getState()).last_retrieved_at) < Calls.RefreshConfigMillis) {
|
||||
return {} as GenericAction;
|
||||
@@ -34,28 +52,27 @@ export function loadConfig(force = false): ActionFunc {
|
||||
dispatch(logError(error));
|
||||
|
||||
// Reset the config to the default (off) since it looks like Calls is not enabled.
|
||||
return {
|
||||
dispatch({
|
||||
type: CallsTypes.RECEIVED_CONFIG,
|
||||
data: {...DefaultServerConfig, last_retrieved_at: Date.now()},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CONFIG,
|
||||
data: {...data, last_retrieved_at: Date.now()},
|
||||
};
|
||||
data = {...data, last_retrieved_at: Date.now()};
|
||||
dispatch({type: CallsTypes.RECEIVED_CONFIG, data});
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadCalls(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<GenericAction> => {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
let resp = [];
|
||||
try {
|
||||
resp = await Client4.getCalls();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {} as GenericAction;
|
||||
return {};
|
||||
}
|
||||
|
||||
const callsResults: Dictionary<Call> = {};
|
||||
@@ -76,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;
|
||||
@@ -86,21 +104,46 @@ export function loadCalls(): ActionFunc {
|
||||
enabled: enabledChannels,
|
||||
};
|
||||
|
||||
return {type: CallsTypes.RECEIVED_CALLS, data};
|
||||
dispatch({type: CallsTypes.RECEIVED_CALLS, data});
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function batchLoadCalls(forceConfig = false): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
const promises = [dispatch(loadConfig(forceConfig)), dispatch(loadCalls())];
|
||||
Promise.all(promises).then((actions: Array<Awaited<GenericAction>>) => {
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CALLS'));
|
||||
const res = await dispatch(checkIsCallsPluginEnabled());
|
||||
if (!res.data) {
|
||||
// Calls is not enabled.
|
||||
return {};
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(loadConfig(forceConfig));
|
||||
dispatch(loadCalls());
|
||||
});
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
export function checkIsCallsPluginEnabled(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getPluginsManifests();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {} as GenericAction;
|
||||
}
|
||||
|
||||
const enabled = data.findIndex((m) => m.id === Calls.PluginId) !== -1;
|
||||
dispatch({type: CallsTypes.RECEIVED_PLUGIN_ENABLED, data: enabled});
|
||||
|
||||
return {data: enabled};
|
||||
};
|
||||
}
|
||||
|
||||
export function enableChannelCalls(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
try {
|
||||
@@ -135,6 +178,10 @@ export function disableChannelCalls(channelId: string): ActionFunc {
|
||||
|
||||
export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
// Edge case: calls was disabled when app loaded, and then enabled, but app hasn't
|
||||
// reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet)
|
||||
dispatch(checkIsCallsPluginEnabled());
|
||||
|
||||
const hasPermission = await hasMicrophonePermission(intl);
|
||||
if (!hasPermission) {
|
||||
return {error: 'no permissions to microphone, unable to start call'};
|
||||
@@ -154,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));
|
||||
@@ -177,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 {};
|
||||
};
|
||||
}
|
||||
@@ -225,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 '';
|
||||
}
|
||||
@@ -216,6 +227,16 @@ function speakerphoneOn(state = false, action: GenericAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function pluginEnabled(state = false, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.RECEIVED_PLUGIN_ENABLED: {
|
||||
return action.data;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
calls,
|
||||
enabled,
|
||||
@@ -223,4 +244,5 @@ export default combineReducers({
|
||||
screenShareURL,
|
||||
speakerphoneOn,
|
||||
config,
|
||||
pluginEnabled,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -61,3 +64,53 @@ export function isSupportedServer(state: GlobalState) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,15 +11,17 @@ export type CallsState = {
|
||||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
config: ServerConfig;
|
||||
pluginEnabled: boolean;
|
||||
}
|
||||
|
||||
export type Call = {
|
||||
participants: Dictionary<CallParticipant>;
|
||||
participants: Dictionary<CallParticipant>;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
speakers: string[];
|
||||
screenOn: string;
|
||||
threadId: string;
|
||||
creatorId: string;
|
||||
}
|
||||
|
||||
export type CallParticipant = {
|
||||
@@ -48,6 +50,7 @@ export type ServerCallState = {
|
||||
states: ServerUserState[];
|
||||
thread_id: string;
|
||||
screen_sharing_id: string;
|
||||
creator_id: string;
|
||||
}
|
||||
|
||||
export type VoiceEventData = {
|
||||
@@ -59,6 +62,8 @@ export type ServerConfig = {
|
||||
ICEServers: string[];
|
||||
AllowEnableCalls: boolean;
|
||||
DefaultEnabled: boolean;
|
||||
MaxCallParticipants: number;
|
||||
sku_short_name: string;
|
||||
last_retrieved_at: number;
|
||||
}
|
||||
|
||||
@@ -66,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;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import Calls from '@constants/calls';
|
||||
import {encode} from '@msgpack/msgpack/dist';
|
||||
|
||||
export default class WebSocketClient extends EventEmitter {
|
||||
private ws: WebSocket | null;
|
||||
private seqNo = 0;
|
||||
private connID = '';
|
||||
private eventPrefix = 'custom_com.mattermost.calls';
|
||||
private eventPrefix = `custom_${Calls.PluginId}`;
|
||||
|
||||
constructor(connURL: string) {
|
||||
super();
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class ChannelAndroid extends ChannelBase {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {theme, viewingGlobalThreads, isSupportedServerCalls} = this.props;
|
||||
const {theme, viewingGlobalThreads, isCallsEnabled} = this.props;
|
||||
let component;
|
||||
|
||||
if (viewingGlobalThreads) {
|
||||
@@ -106,11 +106,12 @@ export default class ChannelAndroid extends ChannelBase {
|
||||
{component}
|
||||
<NetworkIndicator/>
|
||||
<AnnouncementBanner/>
|
||||
{isSupportedServerCalls &&
|
||||
{isCallsEnabled &&
|
||||
<FloatingCallContainer>
|
||||
<JoinCall/>
|
||||
<CurrentCall/>
|
||||
</FloatingCallContainer>}
|
||||
</FloatingCallContainer>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class ChannelIOS extends ChannelBase {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentChannelId, theme, viewingGlobalThreads, isSupportedServerCalls} = this.props;
|
||||
const {currentChannelId, theme, viewingGlobalThreads, isCallsEnabled} = this.props;
|
||||
|
||||
let component;
|
||||
let renderDraftArea = false;
|
||||
@@ -85,11 +85,12 @@ export default class ChannelIOS extends ChannelBase {
|
||||
<>
|
||||
<AnnouncementBanner/>
|
||||
<NetworkIndicator/>
|
||||
{isSupportedServerCalls &&
|
||||
{isCallsEnabled &&
|
||||
<FloatingCallContainer>
|
||||
<JoinCall/>
|
||||
<CurrentCall/>
|
||||
</FloatingCallContainer>}
|
||||
</FloatingCallContainer>
|
||||
}
|
||||
</>
|
||||
);
|
||||
const header = (
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class ChannelBase extends PureComponent {
|
||||
viewingGlobalThreads: PropTypes.bool,
|
||||
collapsedThreadsEnabled: PropTypes.bool.isRequired,
|
||||
isSupportedServerCalls: PropTypes.bool.isRequired,
|
||||
isCallsEnabled: PropTypes.bool.isRequired,
|
||||
selectedPost: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
channel_id: PropTypes.string.isRequired,
|
||||
|
||||
@@ -18,7 +18,10 @@ import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
|
||||
import {batchLoadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {isSupportedServer as isSupportedServerForCalls} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {
|
||||
isCallsPluginEnabled,
|
||||
isSupportedServer as isSupportedServerForCalls,
|
||||
} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
|
||||
import Channel from './channel';
|
||||
@@ -44,6 +47,7 @@ function mapStateToProps(state) {
|
||||
const currentChannelId = currentTeam?.delete_at === 0 ? getCurrentChannelId(state) : '';
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const isSupportedServerCalls = isSupportedServerForCalls(state);
|
||||
const isCallsEnabled = isCallsPluginEnabled(state);
|
||||
|
||||
return {
|
||||
currentChannelId,
|
||||
@@ -58,6 +62,7 @@ function mapStateToProps(state) {
|
||||
theme: getTheme(state),
|
||||
viewingGlobalThreads: collapsedThreadsEnabled && getViewingGlobalThreads(state),
|
||||
isSupportedServerCalls,
|
||||
isCallsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.579.0)
|
||||
aws-sdk-core (3.130.1)
|
||||
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.0)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
@@ -36,7 +36,7 @@ GEM
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.2)
|
||||
excon (0.92.3)
|
||||
faraday (1.10.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
@@ -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 = 392;
|
||||
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 = 392;
|
||||
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.51.1</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -37,7 +37,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>392</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.51.1</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>392</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.51.1</string>
|
||||
<string>1.53.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>392</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.51.1",
|
||||
"version": "1.53.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "1.51.1",
|
||||
"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