Compare commits

...

26 Commits

Author SHA1 Message Date
Elias Nahum
7baf943038 Bump app version to 1.54.0 build 410 (#6444)
* Bump app version number to  1.54.0

* Bump app build number to  410
2022-07-14 10:47:23 -04:00
Mattermost Build
8392bcb4f8 [MM-45001] Add support for generating short-lived TURN credentials (#6404) (#6442)
* Generate TURN credentials if needed

* Reduce calls to getState

(cherry picked from commit ed6daffa0c)

Co-authored-by: Claudio Costa <cstcld91@gmail.com>
2022-06-30 10:17:09 +02:00
Anurag Shivarathri
11136e7cf6 Latex fix (#6435) 2022-06-28 08:46:06 -04:00
Kaya Zeren
cd70d9067a Translated using Weblate (Turkish)
Currently translated at 100.0% (791 of 791 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/tr/
2022-06-28 14:48:39 +03:00
Rodrigo Pimentel Piñero
d834c3042e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (791 of 791 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/pt_BR/
2022-06-28 14:48:39 +03:00
MArtin Johnson
14b1227c16 Translated using Weblate (Swedish)
Currently translated at 100.0% (791 of 791 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/sv/
2022-06-21 09:04:09 +02:00
Claudio Costa
5b0b1e9af8 Add support for using plugin when MM is served under a subpath (#6403) 2022-06-20 17:07:06 +02:00
Mattermod
41b571f137 Update Licences at Notice.txt to reflect dependency changes. (#6391) 2022-06-16 18:03:13 -04:00
Claudio Costa
ee38dd39ea Add support for ICEServersConfigs (#6371) 2022-06-16 14:15:17 +02:00
Elias Nahum
5e7f368583 Exclude Beta builds from v1 (#6380) 2022-06-13 18:57:23 -04:00
Guillermo Vayá
c7586aa4d8 Merge pull request #6381 from weblate/weblate-mattermost-mattermost-mobile_master
Translations update from Mattermost Weblate
2022-06-13 18:50:09 +02:00
MArtin Johnson
094621b99b Translated using Weblate (Swedish)
Currently translated at 100.0% (791 of 791 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/sv/
2022-06-13 18:03:40 +02:00
Elias Nahum
c5d5e25426 Fix .gitignore 2022-06-09 13:17:24 -04:00
Mustafa Kara
901dae29e3 Merge pull request #6361 from mattermost/chore/create-notice-txt-pipeline-configuration
Create configuration folder for notice.txt generation
2022-06-09 09:48:57 +03:00
Mustafa Kara
8f9de69b4d Apply suggestions from code review
Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2022-06-08 15:41:40 +03:00
Mustafa Kara
10faa0f37d Apply changes from code review
Signed-off-by: Mustafa Kara <mustafa.kara@mattermost.com>
2022-06-08 09:06:41 +03:00
Mustafa Kara
3c40a6db30 Apply changes from code review
Signed-off-by: Mustafa Kara <mustafa.kara@mattermost.com>
2022-06-08 09:06:29 +03:00
Mustafa Kara
7db68a5292 Create configuration folder for notice.txt generation
Signed-off-by: Mustafa Kara <mustafa.kara@mattermost.com>
2022-06-08 09:06:00 +03:00
Elias Nahum
cb1773bda5 Bump version to 1.53.0 Build 404 (#6345)
* Bump app version number to  1.53.0

* Bump app build number to  404
2022-06-03 11:30:07 -04:00
Mustafa Kara
a10a21f152 Merge pull request #6333 from mattermost/chore/update-notice-txt-2022-06-02
Update Licences at Notice.txt to reflect dependency changes.
2022-06-03 09:52:40 +03:00
Claudio Costa
bb655c8c60 [MM-44651] Implement MaxCallParticipants config setting (#6334)
* Implement MaxCallParticipants config setting

* Add test
2022-06-03 08:18:12 +02:00
mattermod
c2de372504 Update Licences at Notice.txt to reflect dependency changes. 2022-06-02 10:24:22 +00:00
Claudio Costa
23509cbb83 [MM-44155] Handle call_end event (#6316)
* Handle call_end event

* exit call screen on call end; /call end for mobile

* handle permissions before sending cmd to server; handle error

Co-authored-by: Christopher Poile <cpoile@gmail.com>
2022-06-02 10:59:11 +02:00
Christopher Poile
c74cd14713 MM-44546 -- Calls: Cloud freemium limits (#6318)
* remove API call to config/pass iceServers; leave call on ws error

* cloud limits

* fix makeStyleSheetFromTheme

* revert podfile & package-lock diffs

* update snapshots

* edge case of cloud server on calls 0.5.3
2022-06-01 19:21:10 -04:00
Tom De Moor
1d299d02f0 Deleted translation using Weblate (Macedonian) 2022-06-01 09:19:04 +02:00
Tom De Moor
0e1d0d78ae Added translation using Weblate (Macedonian) 2022-06-01 09:19:04 +02:00
47 changed files with 1148 additions and 744 deletions

View File

@@ -286,14 +286,6 @@ jobs:
name: Post results to Mattermost
command: go run ../security-automation-config/dependency-check/post_results.go
build-android-beta:
executor: android
steps:
- build-android
- persist
- save:
filename: "*.apk"
build-android-release:
executor: android
steps:
@@ -333,14 +325,6 @@ jobs:
- save:
filename: "*.apk"
build-ios-beta:
executor: ios
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-release:
executor: ios
steps:
@@ -416,16 +400,6 @@ jobs:
target: android
file: "*.apk"
deploy-android-beta:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
deploy-ios-release:
executor: ios
steps:
@@ -434,14 +408,6 @@ jobs:
target: ios
file: "*.ipa"
deploy-ios-beta:
executor: ios
steps:
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
github-release:
executor:
name: android
@@ -485,27 +451,6 @@ workflows:
- /^build-android-\d+$/
- /^build-android-release-\d+$/
- build-android-beta:
context: mattermost-mobile-android-beta
requires:
- test
filters:
branches:
only:
- /^build-\d+$/
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
- deploy-android-beta:
context: mattermost-mobile-android-beta
requires:
- build-android-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
- build-ios-release:
context: mattermost-mobile-ios-release
requires:
@@ -527,27 +472,6 @@ workflows:
- /^build-ios-\d+$/
- /^build-ios-release-\d+$/
- build-ios-beta:
context: mattermost-mobile-ios-beta
requires:
- test
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- deploy-ios-beta:
context: mattermost-mobile-ios-beta
requires:
- build-ios-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- build-android-pr:
context: mattermost-mobile-android-pr
requires:
@@ -590,7 +514,6 @@ workflows:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- /^build-ios-sim-\d+$/
- github-release:

9
.gitignore vendored
View File

@@ -14,7 +14,7 @@ env.d.ts
# Xcode
#
build/
ios/build/*
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -43,6 +43,8 @@ ios/Pods
local.properties
*.iml
android/app/bin
android/app/build
android/build
.settings
.project
.classpath
@@ -103,3 +105,8 @@ detox/detox_pixel_4_xl_api_30
#editor-settings
.vscode
.scannerwork
# Notice.txt generation
!build/notice-file

File diff suppressed because it is too large Load Diff

View File

@@ -131,8 +131,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 398
versionName "1.52.0"
versionCode 410
versionName "1.54.0"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,7 @@ export default class RemoveMarkdown extends React.PureComponent {
channelLink: Renderer.forwardChildren,
emoji: this.renderNull,
hashtag: Renderer.forwardChildren,
latexinline: Renderer.forwardChildren,
paragraph: Renderer.forwardChildren,
heading: Renderer.forwardChildren,

View File

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

View File

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

View File

@@ -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,20 @@ 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'},
);
};
genTURNCredentials = async () => {
return this.doFetch(
`${this.getCallsRoute()}/turn-credentials`,
{method: 'get'},
);
};
};
export default ClientCalls;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,8 @@ import {
} from 'react-native-webrtc';
import {Client4} from '@client/rest';
import {WebsocketEvents} from '@constants';
import {ICEServersConfigs} from '@mmproducts/calls/store/types/calls';
import Peer from './simple-peer';
import WebSocketClient from './websocket';
@@ -20,12 +23,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: ICEServersConfigs, 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 {
@@ -40,13 +44,18 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
console.log('Unable to get media device:', err); // eslint-disable-line no-console
}
const ws = new WebSocketClient(Client4.getWebSocketUrl());
const ws = new WebSocketClient(Client4.getWebSocketUrl(), Client4.getToken());
const disconnect = () => {
if (!isClosed) {
ws.close();
}
if (onCallEnd) {
onCallEnd.remove();
onCallEnd = null;
}
streams.forEach((s) => {
s.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
@@ -65,6 +74,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 +134,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', {

View File

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

View File

@@ -21,6 +21,8 @@ import {
} from 'react-native-webrtc';
import stream from 'readable-stream';
import {ICEServersConfigs} from '@mmproducts/calls/store/types/calls';
const queueMicrotask = (callback: any) => {
Promise.resolve().then(callback).catch((e) => setTimeout(() => {
throw e;
@@ -94,7 +96,7 @@ export default class Peer extends stream.Duplex {
private pc: RTCPeerConnection|null = null;
private onFinishBound?: () => void;
constructor(localStream: MediaStream | null, iceServers?: string[]) {
constructor(localStream: MediaStream | null, iceServers: ICEServersConfigs) {
super({allowHalfOpen: false});
this.streams = localStream ? [localStream] : [];
@@ -104,21 +106,10 @@ export default class Peer extends stream.Duplex {
};
const connConfig = {
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
],
},
],
iceServers,
sdpSemantics: 'unified-plan',
};
if (iceServers && iceServers.length > 0) {
connConfig.iceServers[0].urls = iceServers;
}
try {
this.pc = new RTCPeerConnection(connConfig);
} catch (err) {

View File

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

View File

@@ -34,7 +34,10 @@ jest.mock('@client/rest', () => ({
},
]),
getCallsConfig: jest.fn(() => ({
ICEServers: ['mattermost.com'],
ICEServersConfigs: [{
urls: 'stun:stun1.example.com',
},
],
AllowEnableCalls: true,
DefaultEnabled: true,
last_retrieved_at: 1234,
@@ -117,11 +120,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 () => {

View File

@@ -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,17 @@ 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,
getICEServersConfigs,
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 +94,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 +202,16 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc
dispatch(setSpeakerphoneOn(false));
try {
ws = await newClient(channelId, () => null, setScreenShareURL);
const state = getState();
const iceConfigs = [...getICEServersConfigs(state)];
if (getConfig(state).NeedsTURNCredentials) {
iceConfigs.push(...await Client4.genTURNCredentials());
}
ws = await newClient(channelId, iceConfigs, () => {
dispatch(setSpeakerphoneOn(false));
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
}, setScreenShareURL);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
@@ -212,13 +234,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 +280,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 {};
};
}

View File

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

View File

@@ -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 = {
@@ -398,7 +408,7 @@ describe('Reducers.calls.config', () => {
const testAction = {
type: CallsTypes.RECEIVED_CONFIG,
data: {
ICEServers: ['google.com'],
ICEServers: ['stun:stun.example.com'],
AllowEnableCalls: true,
DefaultEnabled: true,
last_retrieved_at: 123,

View File

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

View File

@@ -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,162 @@ 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);
});
it('getICEServersConfigs', () => {
assert.deepEqual(Selectors.getICEServersConfigs(testState), []);
// backwards compatible case, no ICEServersConfigs present.
let newState = {
...testState,
entities: {
...testState.entities,
calls: {
...testState.entities.calls,
config: {
...testState.entities.calls.config,
ICEServers: ['stun:stun1.example.com'],
},
},
},
};
assert.deepEqual(Selectors.getICEServersConfigs(newState), [{urls: ['stun:stun1.example.com']}]);
// ICEServersConfigs defined case
newState = {
...testState,
entities: {
...testState.entities,
calls: {
...testState.entities.calls,
config: {
...testState.entities.calls.config,
ICEServers: ['stun:stun1.example.com'],
ICEServersConfigs: [
{urls: 'stun:stun1.example.com'},
{urls: 'turn:turn.example.com', username: 'username', credentail: 'password'},
],
},
},
},
};
assert.deepEqual(Selectors.getICEServersConfigs(newState), [
{urls: 'stun:stun1.example.com'},
{urls: 'turn:turn.example.com', username: 'username', credentail: 'password'},
]);
});
});

View File

@@ -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, ICEServersConfigs} from '@mmproducts/calls/store/types/calls';
export function getConfig(state: GlobalState) {
return state.entities.calls.config;
@@ -65,3 +68,71 @@ 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;
},
);
export const getICEServersConfigs: (state: GlobalState) => ICEServersConfigs = createSelector(
getConfig,
(config) => {
// if ICEServersConfigs is set, we can trust this to be complete and
// coming from an updated API.
if (config.ICEServersConfigs?.length > 0) {
return config.ICEServersConfigs;
}
// otherwise we revert to using the now deprecated field.
if (config.ICEServers?.length > 0) {
return [
{
urls: config.ICEServers,
},
];
}
return [];
},
);

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ConfigurationParamWithUrls, ConfigurationParamWithUrl} from 'react-native-webrtc';
import {UserProfile} from '@mm-redux/types/users';
import {Dictionary} from '@mm-redux/types/utilities';
@@ -15,12 +17,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 +52,7 @@ export type ServerCallState = {
states: ServerUserState[];
thread_id: string;
screen_sharing_id: string;
creator_id: string;
}
export type VoiceEventData = {
@@ -58,14 +62,24 @@ export type VoiceEventData = {
export type ServerConfig = {
ICEServers: string[];
ICEServersConfigs: ICEServersConfigs;
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
MaxCallParticipants: number;
NeedsTURNCredentials: boolean;
sku_short_name: string;
last_retrieved_at: number;
}
export const DefaultServerConfig = {
ICEServers: [],
ICEServersConfigs: [],
AllowEnableCalls: false,
DefaultEnabled: false,
MaxCallParticipants: 0,
NeedsTURNCredentials: false,
sku_short_name: '',
last_retrieved_at: 0,
} as ServerConfig;
export type ICEServersConfigs = Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;

View File

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

View File

@@ -7,13 +7,14 @@ import {encode} from '@msgpack/msgpack/dist';
export default class WebSocketClient extends EventEmitter {
private ws: WebSocket | null;
private seqNo = 0;
private seqNo = 1;
private connID = '';
private eventPrefix = `custom_${Calls.PluginId}`;
constructor(connURL: string) {
constructor(connURL: string, authToken: string) {
super();
this.ws = new WebSocket(connURL);
this.ws = new WebSocket(connURL, [], {headers: {authorization: `Bearer ${authToken}`}});
this.ws.onerror = (err) => {
this.emit('error', err);
@@ -73,6 +74,7 @@ export default class WebSocketClient extends EventEmitter {
seq: this.seqNo++,
data,
};
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
if (binary) {
this.ws.send(encode(msg));
@@ -87,7 +89,7 @@ export default class WebSocketClient extends EventEmitter {
this.ws.close();
this.ws = null;
}
this.seqNo = 0;
this.seqNo = 1;
this.connID = '';
this.emit('close');
}

View File

@@ -11,35 +11,54 @@
"about.teamEditiont1": "Enterprise Edition",
"about.title": "Sobre {appTitle}",
"announcment_banner.dont_show_again": "Não mostrar novamente",
"api.channel.add_guest.added": "{addedUsername} adicionado ao canal como convidado por {username}.",
"api.channel.add_member.added": "{addedUsername} foi adicionado ao canal por {username}.",
"api.channel.guest_join_channel.post_and_forget": "{username} entrou no canal como convidado.",
"apps.error": "Erro: {error}",
"apps.error.command.field_missing": "Campos obrigatórios ausentes: `{fieldName}`.",
"apps.error.command.same_channel": "Canal repetido para o campo `{fieldName}`: `{option}`.",
"apps.error.command.same_option": "Opção repetida para o campo `{fieldName}`: `{option}`.",
"apps.error.command.same_user": "Usuário repetido para o campo `{fieldName}`: `{option}`.",
"apps.error.command.unknown_channel": "Canal desconhecido para o campo `{fieldName}`: `{option}`.",
"apps.error.command.unknown_option": "Opção desconhecida para o campo `{fieldName}`: `{option}`.",
"apps.error.command.unknown_user": "Usuário desconhecido para o campo `{fieldName}`: `{opção}`.",
"apps.error.form.no_form": "`form` não está definido.",
"apps.error.form.no_lookup": "`procurar` não está definido.",
"apps.error.form.no_source": "`fonte` não está definida.",
"apps.error.form.no_submit": "`enviar ` não está definido",
"apps.error.form.refresh": "Ocorreu um erro ao buscar os campos selecionados. Entre em contato com o desenvolvedor do aplicativo. Detalhes: {details}",
"apps.error.form.refresh_no_refresh": "Atualização chamada no campo sem atualização.",
"apps.error.form.submit.pretext": "Ocorreu um erro ao enviar o modal. Entre em contato com o desenvolvedor do aplicativo. Detalhes: {details}",
"apps.error.lookup.error_preparing_request": "Erro ao preparar solicitação de pesquisa: {errorMessage}",
"apps.error.malformed_binding": "Esta ligação não está formada corretamente. Entre em contato com o desenvolvedor do aplicativo.",
"apps.error.parser": "Erro de análise: {error}",
"apps.error.parser.empty_value": "valores vazios não são permitidos",
"apps.error.parser.execute_non_leaf": "Você deve selecionar um subcomando.",
"apps.error.parser.missing_binding": "Ligações de comando ausentes.",
"apps.error.parser.missing_field_value": "Falta o valor do campo.",
"apps.error.parser.missing_list_end": "Token de fechamento de lista esperado.",
"apps.error.parser.missing_quote": "Aspas duplas correspondentes são esperadas antes do final da entrada.",
"apps.error.parser.missing_source": "O formulário não tem envio nem fonte.",
"apps.error.parser.missing_submit": "Nenhuma chamada de envio em forma ou vinculação.",
"apps.error.parser.missing_tick": "Crase correspondente é esperada antes do final de entrada.",
"apps.error.parser.multiple_equal": "Vários sinais `=` não são permitidos.",
"apps.error.parser.no_argument_pos_x": "Incapaz de identificar o argumento.",
"apps.error.parser.no_bindings": "Sem ligações de comando.",
"apps.error.parser.no_form": "Nenhum formulário encontrado.",
"apps.error.parser.no_match": "`{command}`: Nenhum comando correspondente encontrado neste espaço de trabalho.",
"apps.error.parser.no_slash_start": "O comando deve começar com `/`.",
"apps.error.parser.unexpected_character": "Caracter inesperado.",
"apps.error.parser.unexpected_comma": "Vírgula inesperada.",
"apps.error.parser.unexpected_error": "Erro inesperado.",
"apps.error.parser.unexpected_flag": "O comando não aceita o sinalizador `{flagName}`.",
"apps.error.parser.unexpected_squared_bracket": "Abertura de lista inesperada.",
"apps.error.parser.unexpected_state": "Inacessível: Estado inesperado em matchBinding: `{state}`.",
"apps.error.parser.unexpected_whitespace": "Inacessível: espaço em branco inesperado.",
"apps.error.responses.form.no_form": "O tipo de resposta é `form`, mas nenhum formulário foi incluído na resposta.",
"apps.error.responses.navigate.no_url": "O tipo de resposta é `navigate`, mas nenhuma url foi incluído na resposta.",
"apps.error.responses.unexpected_error": "Recebeu um erro inesperado.",
"apps.error.responses.unexpected_type": "O tipo de resposta do aplicativo não era esperado. Tipo de resposta: {type}.",
"apps.error.responses.unknown_field_error": "Recebeu um erro para um campo desconhecido. Nome do campo: `{field}`. Erro: `{error}`.",
"apps.error.responses.unknown_type": "O tipo de resposta do aplicativo não é compatível. Tipo de resposta: {type}.",
"apps.error.unknown": "Ocorreu um erro desconhecido.",
"apps.suggestion.dynamic.error": "Erro de seleção dinâmica",
@@ -48,6 +67,9 @@
"apps.suggestion.no_static": "Sem opções correspondentes.",
"apps.suggestion.no_suggestion": "Sem sugestões correspondentes.",
"archivedChannelMessage": "Você está vendo um **canal arquivado**. Novas mensagens não podem ser publicadas.",
"camera_type.photo.option": "Capturar foto",
"camera_type.title": "Escolha uma ação",
"camera_type.video.option": "Record Video",
"center_panel.archived.closeChannel": "Fechar Canal",
"channel.channelHasGuests": "Este canal tem convidados",
"channel.hasGuests": "Este grupo de mensagem tem convidados",
@@ -67,6 +89,7 @@
"channel_loader.someone": "Alguém",
"channel_members_modal.remove": "Remover",
"channel_modal.cancel": "Cancelar",
"channel_modal.channelType": "Modelo",
"channel_modal.descriptionHelp": "Descreva como este canal deve ser utilizado.",
"channel_modal.header": "Cabeçalho",
"channel_modal.headerEx": "Ex.: \"[Título do Link](http://example.com)\"",
@@ -76,6 +99,8 @@
"channel_modal.optional": "(opcional)",
"channel_modal.purpose": "Propósito",
"channel_modal.purposeEx": "Ex.: \"Um canal para arquivar bugs e melhorias\"",
"channel_modal.type.private": "Canal privado",
"channel_modal.type.public": "Canal público",
"channel_notifications.ignoreChannelMentions.settings": "Ignorar @channel, @here, @all",
"channel_notifications.muteChannel.settings": "Silenciar o canal",
"channel_notifications.preference.all_activity": "Para todas as atividades",
@@ -120,21 +145,76 @@
"create_comment.addComment": "Adicionar um comentário...",
"create_post.deactivated": "Você está vendo um canal arquivado com um usuário inativo.",
"create_post.write": "Escrever para {channelDisplayName}",
"custom_status.expiry.at": "no",
"custom_status.expiry.until": "Até",
"custom_status.expiry_dropdown.custom": "Personalizado",
"custom_status.expiry_dropdown.date_and_time": "Data e hora",
"custom_status.expiry_dropdown.dont_clear": "Não limpe",
"custom_status.expiry_dropdown.four_hours": "4 horas",
"custom_status.expiry_dropdown.one_hour": "1 hora",
"custom_status.expiry_dropdown.thirty_minutes": "30 minutos",
"custom_status.expiry_dropdown.this_week": "Essa semana",
"custom_status.expiry_dropdown.today": "Hoje",
"custom_status.expiry_time.today": "Hoje",
"custom_status.expiry_time.tomorrow": "Amanhã",
"custom_status.failure_message": "Falha ao atualizar o status. Tente novamente",
"custom_status.set_status": "Definir um status",
"custom_status.suggestions.in_a_meeting": "Em uma reunião",
"custom_status.suggestions.on_a_vacation": "De férias",
"custom_status.suggestions.out_for_lunch": "Almoçando",
"custom_status.suggestions.out_sick": "Doente",
"custom_status.suggestions.recent_title": "RECENTE",
"custom_status.suggestions.title": "SUGESTÕES",
"custom_status.suggestions.working_from_home": "Trabalhando em casa",
"date_separator.today": "Hoje",
"date_separator.yesterday": "Ontem",
"edit_post.editPost": "Editar o post...",
"edit_post.save": "Salvar",
"emoji_picker.activities": "Atividades",
"emoji_picker.animals-nature": "Animais e Natureza",
"emoji_picker.custom": "Personalizado",
"emoji_picker.flags": "Bandeiras",
"emoji_picker.food-drink": "Comida e bebida",
"emoji_picker.objects": "Objetos",
"emoji_picker.people-body": "Pessoas e Corpo",
"emoji_picker.recent": "Recente",
"emoji_picker.searchResults": "Resultados da Pesquisa",
"emoji_picker.smileys-emotion": "Sorrisos e Emoções",
"emoji_picker.symbols": "Símbolos",
"emoji_picker.travel-places": "Viagens e lugares",
"emoji_skin.dark_skin_tone": "tom de pele escuro",
"emoji_skin.default": "tom de pele padrão",
"emoji_skin.light_skin_tone": "tom de pele claro",
"emoji_skin.medium_dark_skin_tone": "pele morena escura",
"emoji_skin.medium_light_skin_tone": "tom de pele claro médio",
"emoji_skin.medium_skin_tone": "tom de pele médio",
"file_upload.fileAbove": "Arquivos devem ter menos de {max}",
"friendly_date.daysAgo": "{count} {count, plural, one {day} other {days}} atrás",
"friendly_date.hoursAgo": "{count} {count, plural, one {hour} other {hours}} atrás",
"friendly_date.minsAgo": "{count} {count, plural, one {min} other {mins}} atrás",
"friendly_date.monthsAgo": "{count} {count, plural, one {month} other {months}} atrás",
"friendly_date.now": "Agora",
"friendly_date.yearsAgo": "{count} {count, plural, one {year} other {years}} atrás",
"friendly_date.yesterday": "Ontem",
"gallery.download_file": "Baixar arquivo",
"gallery.footer.channel_name": "Compartilhado em {channelName}",
"gallery.open_file": "Abrir arquivo",
"gallery.unsuppored": "A visualização não é compatível com este tipo de arquivo",
"get_post_link_modal.title": "Copiar Link",
"global_threads.allThreads": "Todos os seus tópicos",
"global_threads.emptyThreads.message": "Todos os tópicos nos quais você é mencionado ou dos quais participou serão exibidos aqui junto com todos os tópicos que você seguiu.",
"global_threads.emptyThreads.title": "Nenhum tópico seguido ainda",
"global_threads.emptyUnreads.message": "Parece que você está todo preso.",
"global_threads.emptyUnreads.title": "Nenhum tópico não lido",
"global_threads.markAllRead.cancel": "Cancelar",
"global_threads.markAllRead.markRead": "Marcar como lido",
"global_threads.markAllRead.message": "Isso limpará qualquer status não lido de todos os seus tópicos mostrados aqui",
"global_threads.markAllRead.title": "Tem certeza de que deseja marcar todos os tópicos como lidos?",
"global_threads.options.mark_as_read": "Marcar como Lido",
"global_threads.options.open_in_channel": "Abrir no canal",
"global_threads.options.title": "AÇÕES DE TÓPICO",
"global_threads.options.unfollow": "Deixar de seguir o tópico",
"global_threads.unreads": "Não lidos",
"integrations.add": "Adicionar",
"intro_messages.anyMember": " Qualquer membro pode participar e ler este canal.",
"intro_messages.beginning": "Início do {name}",
@@ -208,7 +288,7 @@
"mobile.camera_photo_permission_denied_title": "{applicationName} gostaria de acessar sua camera",
"mobile.camera_video_permission_denied_description": "Faça vídeos e envie-os para a sua instância do Mattermost ou salve-os no seu dispositivo. Abra Configurações para conceder ao Mattermost acesso de leitura e gravação à sua câmera.",
"mobile.camera_video_permission_denied_title": "{applicationName} gostaria de acessar sua camera",
"mobile.channel_drawer.search": "Ir para...",
"mobile.channel_drawer.search": "Encontrar canal",
"mobile.channel_info.alertMessageConvertChannel": "Quando você converte {displayName} em um canal privado, o histórico e os membros são preservados. Os arquivos compartilhados publicamente permanecem acessíveis a qualquer pessoa com o link. O ingresso a um canal privado é apenas por convite.\n\nA mudança é permanente e não pode ser desfeita.\n\nTem certeza de que deseja converter {displayName} em um canal privado?",
"mobile.channel_info.alertMessageDeleteChannel": "Você tem certeza que quer arquivar o {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "Você tem certeza que quer deixar o {term} {name}?",
@@ -252,6 +332,9 @@
"mobile.create_channel.public": "Novo Canal Público",
"mobile.create_post.read_only": "Este canal é de apenas leitura",
"mobile.custom_list.no_results": "Nenhum Resultado",
"mobile.custom_status.choose_emoji": "Escolha um emoji",
"mobile.custom_status.clear_after": "Limpar depois",
"mobile.custom_status.modal_confirm": "Feito",
"mobile.display_settings.sidebar": "Barra lateral",
"mobile.display_settings.theme": "Tema",
"mobile.document_preview.failed_description": "Um erro aconteceu durante a abertura do documento. Por favor verifique que você tem o visualizador para {fileType} instalado e tente novamente.\n",
@@ -288,7 +371,7 @@
"mobile.file_upload.disabled": "Os uploads de arquivos do celular estão desabilitados. Entre em contato com o Administrador do Sistema para obter mais detalhes.",
"mobile.file_upload.disabled2": "Os envios de arquivos do celular estão desabilitados.",
"mobile.file_upload.library": "Biblioteca de Fotos",
"mobile.file_upload.max_warning": "Upload limitado ao máximo de 5 arquivos.",
"mobile.file_upload.max_warning": "Uploads limitados a {count} máximo de arquivos.",
"mobile.file_upload.unsupportedMimeType": "Somente imagens em BMP, JPG ou PNG podem ser usadas como imagem do perfil.",
"mobile.file_upload.video": "Galeria de Videos",
"mobile.files_paste.error_description": "Ocorreu um erro enquanto colava o arquivo. Por favor tente novamente.",
@@ -296,9 +379,11 @@
"mobile.files_paste.error_title": "Colar falhou",
"mobile.flagged_posts.empty_description": "As mensagens salvas são visíveis apenas para você. Marque mensagens para acompanhamento ou salve algo para mais tarde mantendo uma mensagem pressionada e escolhendo Salvar no menu.",
"mobile.flagged_posts.empty_title": "Nenhuma mensagem salva",
"mobile.forms.select.done": "Feito",
"mobile.gallery.title": "{index} de {total}",
"mobile.general.error.title": "Erro",
"mobile.help.title": "Ajuda",
"mobile.interactive_dialog.defaultSubmit": "Enviar",
"mobile.intro_messages.DM": "Este é o início do seu histórico de mensagens diretas com {teammate}. Mensagens diretas e arquivos compartilhados aqui não são mostrados para pessoas de fora desta área.",
"mobile.intro_messages.default_message": "Este é o primeiro canal da equipe veja quando eles se registrarem - use para postar atualizações que todos devem saber.",
"mobile.intro_messages.default_welcome": "Bem-vindo ao {name}!",
@@ -330,6 +415,8 @@
"mobile.message_length.message": "Sua mensagem atual é muito grande. Contagem corrente de caracters: {max}/{count}",
"mobile.message_length.message_split_left": "Mensagem excede o limite de caracteres",
"mobile.message_length.title": "Tamanho da Mensagem",
"mobile.microphone_permission_denied_description": "Para participar desta chamada, abra Configurações para conceder ao Mattermost acesso ao seu microfone.",
"mobile.microphone_permission_denied_title": "{applicationName} gostaria de acessar seu microfone",
"mobile.more_dms.add_more": "Você pode adicionar mais {remaining, number} usuários",
"mobile.more_dms.cannot_add_more": "Você não pode adicionar mais usuários",
"mobile.more_dms.one_more": "Você não pode adicionar mais 1 usuário",
@@ -349,6 +436,7 @@
"mobile.notification_settings.email": "E-mail",
"mobile.notification_settings.email.send": "ENVIAR NOTIFICAÇÕES POR EMAIL",
"mobile.notification_settings.email_title": "Notificações por Email",
"mobile.notification_settings.mentions": "Menções",
"mobile.notification_settings.mentions.channelWide": "Menções de todo canal",
"mobile.notification_settings.mentions.reply_title": "Enviar notificações de resposta para",
"mobile.notification_settings.mentions.sensitiveName": "Seu primeiro nome em maiúsculas e minúsculas",
@@ -359,6 +447,10 @@
"mobile.notification_settings.modal_cancel": "CANCELAR",
"mobile.notification_settings.modal_save": "SALVAR",
"mobile.notification_settings.ooo_auto_responder": "Respostas Automáticas para Mensagens Diretas",
"mobile.notification_settings.push_threads.description": "Notificar-me sobre todas as respostas aos tópicos que estou seguindo",
"mobile.notification_settings.push_threads.info": "Quando ativado, qualquer resposta a um tópico que você está seguindo enviará uma notificação por push móvel.",
"mobile.notification_settings.push_threads.title": "NOTIFICAÇÕES DE RESPOSTA DE TÓPICOS",
"mobile.notification_settings.push_threads.title_android": "Notificações de resposta do tópico",
"mobile.notification_settings.save_failed_description": "As configurações de notificação não conseguiram salvar devido a um problema de conexão. Por favor tente novamente.",
"mobile.notification_settings.save_failed_title": "Problema de conexão",
"mobile.notification_settings_mentions.keywords": "Palavras-chave",
@@ -391,6 +483,8 @@
"mobile.open_dm.error": "Não foi possível entrar nas mensagens diretas com {displayName}. Por favor verifique a sua conexão e tente novamente.",
"mobile.open_gm.error": "Não foi possível abrir uma mensagem em grupo com esses usuários. Por favor verifique sua conexão e tente novamente.",
"mobile.open_unknown_channel.error": "Não é possível entrar no canal. Por favor, limpe o cache e tente novamente.",
"mobile.participants.header": "PARTICIPANTES DO TÓPICO",
"mobile.participants.you": "(você)",
"mobile.permission_denied_dismiss": "Não Permitir",
"mobile.permission_denied_retry": "Configurações",
"mobile.pinned_posts.empty_description": "Fixe mensagens importantes que sejam visíveis para todo o canal. Mantenha uma mensagem pressionada e escolha Fixar no canal para salvá-la aqui.",
@@ -418,6 +512,8 @@
"mobile.post_textbox.entire_channel.message": "Usando @all ou @channel você está enviando notificações para {totalMembers, number} {totalMembers, plural, one {pessoa} other {pessoas}}. Você tem certeza que quer fazer isso?",
"mobile.post_textbox.entire_channel.message.with_timezones": "Usando @all ou @channel você está enviando notificações para {totalMembers, number} {totalMembers, plural, one {pessoa} other {pessoas}} em {timezones, number} {timezones, plural, one {fuso horário} other {fusos horários}}. Você tem certeza que quer fazer isso?",
"mobile.post_textbox.entire_channel.title": "Confirmar o envio de notificações para o canal todo",
"mobile.post_textbox.entire_channel_here.message": "Ao usar @here você está prestes a enviar notificações para {totalMembers, number} {totalMembers, plural, one {person} other {people}}. Você tem certeza de que quer fazer isso?",
"mobile.post_textbox.entire_channel_here.message.with_timezones": "Ao usar @here, você está prestes a enviar notificações para {totalMembers, number} {totalMembers, plural, one {person} other {people}} em {timezones, number} {timezones, plural, one {timezone} other {timezones} }. Você tem certeza de que quer fazer isso?",
"mobile.post_textbox.groups.title": "Confirmar envio de notificações para grupos",
"mobile.post_textbox.multi_group.message.with_timezones": "Ao usar {mentions} e {finalMention}, você está prestes a enviar notificações para pelo menos {totalMembers} pessoas em {timezones, number} {timezones, plural, one {fuso horário} other {fusos horários}}. Você tem certeza de que quer fazer isso?",
"mobile.post_textbox.multi_group.message.without_timezones": "Ao usar {mentions} e {finalMention}, você está prestes a enviar notificações para pelo menos {totalMembers} pessoas. Você tem certeza de que quer fazer isso?",
@@ -448,7 +544,7 @@
"mobile.reset_status.alert_cancel": "Cancelar",
"mobile.reset_status.alert_ok": "Ok",
"mobile.reset_status.title_ooo": "Desativar \"Fora Do Escritório\"?",
"mobile.retry_message": "Atualização de mensagens falharam. Puxe para tentar novamente.",
"mobile.retry_message": "Falha ao buscar mensagens. Toque aqui para tentar novamente.",
"mobile.routes.channelInfo": "Informações",
"mobile.routes.channelInfo.createdBy": "Criado por {creator} em ",
"mobile.routes.channelInfo.delete_channel": "Arquivar Canal",
@@ -460,6 +556,7 @@
"mobile.routes.channel_members.action_message_confirm": "Você tem certeza que quer remover o membro selecionado do canal?",
"mobile.routes.code": "{language} Código",
"mobile.routes.code.noLanguage": "Código",
"mobile.routes.custom_status": "Definir um status",
"mobile.routes.edit_profile": "Editar Perfil",
"mobile.routes.login": "Login",
"mobile.routes.loginOptions": "Selecionador de Login",
@@ -470,10 +567,13 @@
"mobile.routes.sso": "Single Sign-On",
"mobile.routes.table": "Tabela",
"mobile.routes.thread": "Tópico {channelName}",
"mobile.routes.thread_crt": "Tópico",
"mobile.routes.thread_crt.in": "em {channelName}",
"mobile.routes.thread_dm": "Tópico Mensagem Direta",
"mobile.routes.user_profile": "Perfil",
"mobile.routes.user_profile.edit": "Editar",
"mobile.routes.user_profile.local_time": "HORA LOCAL",
"mobile.routes.user_profile.organization": "ORGANIZAÇÃO",
"mobile.routes.user_profile.send_message": "Enviar Mensagem",
"mobile.search.after_modifier_description": "encontrar publicações após uma data específica",
"mobile.search.before_modifier_description": "encontrar publicações antes de uma data específica",
@@ -543,6 +643,7 @@
"mobile.user_list.deactivated": "Desativado",
"mobile.user_removed.message": "Você foi removido do canal.",
"mobile.user_removed.title": "Removido de {channelName}",
"mobile.video_playback.download": "Baixar video",
"mobile.video_playback.failed_description": "Ocorreu um erro ao tentar reproduzir o video.\n",
"mobile.video_playback.failed_title": "Erro ao realizar o playback do video",
"mobile.youtube_playback_error.description": "Ocorreu um erro ao tentar executar um vídeo do YouTube.\nDetalhes: {details}",
@@ -556,8 +657,10 @@
"more_channels.dropdownTitle": "Exibir",
"more_channels.noMore": "Não há mais canais para participar",
"more_channels.publicChannels": "Canais Públicos",
"more_channels.sharedChannels": "Canais compartilhados",
"more_channels.showArchivedChannels": "Exibir: Canais Arquivados",
"more_channels.showPublicChannels": "Exibir: Canais Públicos",
"more_channels.showSharedChannels": "Mostrar: canais compartilhados",
"more_channels.title": "Mais Canais",
"msg_typing.areTyping": "{users} e {last} estão digitando...",
"msg_typing.isTyping": "{user} está digitando...",
@@ -606,6 +709,7 @@
"sidebar.channels": "CANAIS PÚBLICOS",
"sidebar.direct": "MENSAGENS DIRETAS",
"sidebar.favorite": "CANAIS FAVORITOS",
"sidebar.favorites": "Favoritos",
"sidebar.pg": "CANAIS PRIVADOS",
"sidebar.types.recent": "ATIVIDADE RECENTE",
"sidebar.unreads": "Mais não lidos",
@@ -628,12 +732,22 @@
"suggestion.mention.morechannels": "Outros Canais",
"suggestion.mention.nonmembers": "Não no Canal",
"suggestion.mention.special": "Menções Especiais",
"suggestion.mention.you": "(você)",
"suggestion.mention.you": " (você)",
"suggestion.search.direct": "Mensagens Diretas",
"suggestion.search.private": "Canais Privados",
"suggestion.search.public": "Canais Públicos",
"terms_of_service.agreeButton": "Eu Concordo",
"terms_of_service.api_error": "Não é possível concluir o pedido. Se esse problema persistir, entre em contato com o Administrador do Sistema.",
"threads": "Tópicos",
"threads.deleted": "Mensagem original excluída",
"threads.follow": "Seguir",
"threads.followMessage": "Seguir mensagem",
"threads.followThread": "Seguir Tópico",
"threads.following": "Seguindo",
"threads.newReplies": "{count} novo {count, plural, one {reply} other {replies}}",
"threads.replies": "{count} {count, plural, one {reply} other {replies}}",
"threads.unfollowMessage": "Deixar de seguir a mensagem",
"threads.unfollowThread": "Deixar de seguir o tópico",
"user.settings.display.clockDisplay": "Exibição do Relógio",
"user.settings.display.custom_theme": "Tema Personalizado",
"user.settings.display.militaryClock": "Relógio de 24 horas (exemplo: 16:00)",
@@ -652,6 +766,7 @@
"user.settings.general.lastName": "Último Nome",
"user.settings.general.nickname": "Apelido",
"user.settings.general.position": "Cargo",
"user.settings.general.status": "Status",
"user.settings.general.username": "Usuário",
"user.settings.modal.display": "Exibição",
"user.settings.modal.notifications": "Notificações",
@@ -663,6 +778,10 @@
"user.settings.notifications.email.never": "Nunca",
"user.settings.notifications.email.send": "Enviar notificações por email",
"user.settings.notifications.emailInfo": "As notificações por email são enviadas para menções e mensagens diretas quando você estiver offline ou ausente por mais de 5 minutos.",
"user.settings.notifications.email_threads.description": "Notifique-me sobre todas as respostas aos tópicos que estou seguindo.",
"user.settings.notifications.email_threads.info": "Quando ativado, qualquer resposta a uma conversa que você está seguindo enviará uma notificação por e-mail.",
"user.settings.notifications.email_threads.title": "NOTIFICAÇÕES DE RESPOSTA DE TÓPICOS",
"user.settings.notifications.email_threads.title_android": "Notificações de resposta do tópico",
"user.settings.notifications.never": "Nunca",
"user.settings.notifications.onlyMentions": "Somente para menções e mensagens diretas",
"user.settings.push_notification.away": "Ausente ou desconectado",

View File

@@ -343,7 +343,7 @@
"mobile.downloader.disabled_title": "Nedladdning inaktiverat",
"mobile.downloader.failed_description": "Ett fel inträffade när filen skulle laddas ner. Kontrollera din anslutning till internet och försök igen.\n",
"mobile.downloader.failed_title": "Nedladdning misslyckades",
"mobile.drawer.teamsTitle": "Grupper",
"mobile.drawer.teamsTitle": "Team",
"mobile.edit_channel": "Spara",
"mobile.edit_post.title": "Editerar meddelande",
"mobile.edit_profile.remove_profile_photo": "Ta bort foto",
@@ -561,8 +561,8 @@
"mobile.routes.login": "Logga in",
"mobile.routes.loginOptions": "Välj inloggningsmetod",
"mobile.routes.mfa": "Flerfaktorsautentisering",
"mobile.routes.selectChannel": "Ändra kanalnamn",
"mobile.routes.selectTeam": "Välj grupp",
"mobile.routes.selectChannel": "Välj kanal",
"mobile.routes.selectTeam": "Välj team",
"mobile.routes.settings": "Inställningar",
"mobile.routes.sso": "Auto-inloggning",
"mobile.routes.table": "Tabell",
@@ -586,7 +586,7 @@
"mobile.search.on_modifier_description": "för att hitta inlägg före ett visst datum",
"mobile.search.recent_title": "Senaste sökningar",
"mobile.select_team.guest_cant_join_team": "Ditt gästkonto finns inte i något team eller kanal. Kontakta en administratör.",
"mobile.select_team.join_open": "Öppna grupper du kan gå med i",
"mobile.select_team.join_open": "Öppna team du kan gå med i",
"mobile.select_team.no_teams": "Det finns inget team tillgängligt för dig att ansluta till.",
"mobile.server_link.error.text": "Länken kunde inte hittas på servern.",
"mobile.server_link.error.title": "Länk-fel",

View File

@@ -107,7 +107,7 @@
"channel_notifications.preference.global_default": "Genel varsayılan ({notifyLevel})",
"channel_notifications.preference.header": "Bildirimler gönderimi",
"channel_notifications.preference.never": "Asla",
"channel_notifications.preference.only_mentions": "Yalnız anmalar ve doğrudan iletiler",
"channel_notifications.preference.only_mentions": "Yalnızca anmalar ve doğrudan iletiler",
"channel_notifications.preference.save_error": "Bildirim ayarı kaydedilemedi. Lütfen bağlantınızı denetleyip yeniden deneyin.",
"combined_system_message.added_to_channel.many_expanded": "{users} ve {lastUser} {actor} tarafından **kanala eklendi**.",
"combined_system_message.added_to_channel.one": "{firstUser} {actor} tarafından **kanala eklendi**.",
@@ -202,8 +202,8 @@
"gallery.unsuppored": "Bu dosya türü ön izlenemiyor",
"get_post_link_modal.title": "Bağlantıyı kopyala",
"global_threads.allThreads": "Tüm konularınız",
"global_threads.emptyThreads.message": "Bahsettiğiniz veya katıldığınız tüm konular, takip ettiğiniz tüm konularla birlikte burada görüntülenir.",
"global_threads.emptyThreads.title": "Takip edilen bir konu yok",
"global_threads.emptyThreads.message": "Bahsettiğiniz veya katıldığınız tüm konular, izlediğiniz tüm konularla birlikte burada görüntülenir.",
"global_threads.emptyThreads.title": "İzlenen bir konu yok",
"global_threads.emptyUnreads.message": "Tüm konulara bakmışsınız gibi görünüyor.",
"global_threads.emptyUnreads.title": "Okunmamış bir konu yok",
"global_threads.markAllRead.cancel": "İptal",
@@ -222,7 +222,7 @@
"intro_messages.creatorPrivate": "{name} özel kanalı, {creator} tarafından {date} tarihinde başlatılmış.",
"intro_messages.group_message": "Bu takım arkadaşlarınız ile doğrudan ileti geçmişinizin başlangıcı. Bu bölüm dışındaki kişiler burada paylaşılan doğrudan ileti ve dosyaları göremez.",
"intro_messages.noCreator": "{date} tarihinde oluşturulmuş {name} kanalının başlangıcı.",
"intro_messages.onlyInvited": " Bu özel kanalı yalnız çağrılmış üyeler görüntüleyebilir.",
"intro_messages.onlyInvited": " Bu özel kanalı yalnızca çağrılmış üyeler görüntüleyebilir.",
"last_users_message.added_to_channel.type": "{actor} tarafından **kanala eklendiniz**.",
"last_users_message.added_to_team.type": "{actor} tarafından **takıma eklendiniz**.",
"last_users_message.first": "{firstUser} ve ",
@@ -289,7 +289,7 @@
"mobile.camera_video_permission_denied_description": "Görüntü çekin ve Mattermost kopyanıza yükleyin ya da aygıtınıza kaydedin. Mattermost uygulamasına kameranızı okuma ve yazma izni vermek için ayarlarıın.",
"mobile.camera_video_permission_denied_title": "{applicationName} kameranıza erişmek istiyor",
"mobile.channel_drawer.search": "Kanal ara",
"mobile.channel_info.alertMessageConvertChannel": "{displayName} özel bir kanala dönüştürüldüğünde, geçmiş iletiler ve üyelikler korunur. Herkese açık olarak paylaşılmış dosyalara bağlantıya sahip olan herkes erişmeye devam edebilir. Özel kanallara yalnız çağrı ile üye olunabilir. \n \nBu değişiklik kalıcıdır ve geri alınamaz.\n\n{displayName} kanalını özel kanala dönüştürmek istediğinize emin misiniz?",
"mobile.channel_info.alertMessageConvertChannel": "{displayName} özel bir kanala dönüştürüldüğünde, geçmiş iletiler ve üyelikler korunur. Herkese açık olarak paylaşılmış dosyalara bağlantıya sahip olan herkes erişmeye devam edebilir. Özel kanallara yalnızca çağrı ile üye olunabilir. \n \nBu değişiklik kalıcıdır ve geri alınamaz.\n\n{displayName} kanalını özel kanala dönüştürmek istediğinize emin misiniz?",
"mobile.channel_info.alertMessageDeleteChannel": "{term} {name} kanalını arşivlemek istediğinize emin misiniz?",
"mobile.channel_info.alertMessageLeaveChannel": "{term} {name} kanalından ayrılmak istediğinize emin misiniz?",
"mobile.channel_info.alertMessageUnarchiveChannel": "{term} {name} kanalını arşivden çıkarmak istediğinize emin misiniz?",
@@ -372,12 +372,12 @@
"mobile.file_upload.disabled2": "Mobil uygulamadan dosya yüklemek devre dışı bırakılmış.",
"mobile.file_upload.library": "Fotoğraf kitaplığı",
"mobile.file_upload.max_warning": "En fazla {count} dosya yüklenebilir.",
"mobile.file_upload.unsupportedMimeType": "Profil görseli olarak yalnız BMP, JPG ya da PNG dosyaları kullanılabilir.",
"mobile.file_upload.unsupportedMimeType": "Profil görseli olarak yalnızca BMP, JPG ya da PNG dosyaları kullanılabilir.",
"mobile.file_upload.video": "Görüntü kitaplığı",
"mobile.files_paste.error_description": "Dosyalar yapıştırılırken bir sorun çıktı. Lütfen yeniden deneyin.",
"mobile.files_paste.error_dismiss": "Yok say",
"mobile.files_paste.error_title": "Yapıştırılamadı",
"mobile.flagged_posts.empty_description": "Kaydedilmiş iletileri yalnız siz görebilirsiniz. İletileri takip etmek için işaretleyin ya da bir şeyi kaydetmek için ileti üzerine uzun basıp menüden Kaydet komutunu seçin.",
"mobile.flagged_posts.empty_description": "Kaydedilmiş iletileri yalnızca siz görebilirsiniz. İletileri izlemek için işaretleyin ya da bir şeyi kaydetmek için ileti üzerine uzun basıp menüden Kaydet komutunu seçin.",
"mobile.flagged_posts.empty_title": "Kaydedilmiş bir ileti yok",
"mobile.forms.select.done": "Tamam",
"mobile.gallery.title": "{index} / {total}",
@@ -447,8 +447,8 @@
"mobile.notification_settings.modal_cancel": "İPTAL",
"mobile.notification_settings.modal_save": "KAYDET",
"mobile.notification_settings.ooo_auto_responder": "Otomatik doğrudan ileti yanıtları",
"mobile.notification_settings.push_threads.description": "Takip ettiğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin",
"mobile.notification_settings.push_threads.info": "Bu seçenek etkinleştirildiğinde, takip ettiğiniz bir konuya yazılan yanıtlar mobil anında bildirim olarak iletilir.",
"mobile.notification_settings.push_threads.description": "İzlediğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin",
"mobile.notification_settings.push_threads.info": "Bu seçenek etkinleştirildiğinde, izlediğiniz bir konuya yazılan yanıtlar mobil uygulamada anında bildirim olarak iletilir.",
"mobile.notification_settings.push_threads.title": "KONU YANITI BİLDİRİMLERİ",
"mobile.notification_settings.push_threads.title_android": "Konu yanıtı bildirimleri",
"mobile.notification_settings.save_failed_description": "Bir bağlantı sorunu nedeniyle bildirim ayarları kaydedilemedi. Lütfen yeniden deneyin.",
@@ -740,10 +740,10 @@
"terms_of_service.api_error": "İstek yerine getirilemedi. Sorun sürerse sistem yöneticiniz ile görüşün.",
"threads": "Konular",
"threads.deleted": "Özgün ileti silindi",
"threads.follow": "Takip et",
"threads.followMessage": "İletiyi takip et",
"threads.followThread": "Konuyu takip et",
"threads.following": "Takip ediliyor",
"threads.follow": "İzle",
"threads.followMessage": "İletiyi izle",
"threads.followThread": "Konuyu izle",
"threads.following": "İzleniyor",
"threads.newReplies": "{count} yeni {count, plural, one {yanıt} other {yanıt}}",
"threads.replies": "{count} {count, plural, one {yanıt} other {yanıt}}",
"threads.unfollowMessage": "İletiyi takibi bırak",
@@ -778,12 +778,12 @@
"user.settings.notifications.email.never": "Asla",
"user.settings.notifications.email.send": "E-posta bildirimleri gönderilsin",
"user.settings.notifications.emailInfo": "5 dakikadan uzun süre uzak ya da çevrimdışı olduğunuzda anma ve doğrudan iletiler için e-posta bildirimi gönderilir.",
"user.settings.notifications.email_threads.description": "Takip ettiğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin.",
"user.settings.notifications.email_threads.info": "Bu seçenek etkinleştirildiğinde, takip ettiğiniz bir konuya yazılan yanıtlar e-posta bildirimi olarak iletilir.",
"user.settings.notifications.email_threads.description": "İzlediğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin.",
"user.settings.notifications.email_threads.info": "Bu seçenek etkinleştirildiğinde, izlediğiniz bir konuya yazılan yanıtlar e-posta bildirimi olarak iletilir.",
"user.settings.notifications.email_threads.title": "KONU YANITI BİLDİRİMLERİ",
"user.settings.notifications.email_threads.title_android": "Konu yanıtı bildirimleri",
"user.settings.notifications.never": "Asla",
"user.settings.notifications.onlyMentions": "Yalnız anmalar ve doğrudan iletiler için",
"user.settings.notifications.onlyMentions": "Yalnızca anmalar ve doğrudan iletiler için",
"user.settings.push_notification.away": "Uzakta ya da çevrimdışı",
"user.settings.push_notification.disabled": "Anında bildirimler devre dışı bırakılmış",
"user.settings.push_notification.disabled_long": "Anında bildirimler sistem yöneticiniz tarafından devre dışı bırakılmış.",

View File

@@ -0,0 +1,45 @@
# Notice.txt File Configuration
We are automatically generating Notice.txt by using first-level dependencies of the project. The related pipeline uses `config.yaml` stored in this folder.
## Configuration
Notice.txt will be always generated from first-level dependencies scanned from package.json files. For some cases it may be useful to define
the dependency to add its licence to the Notice.txt even if it is not a first-level dependency or development dependency.
For instance, at the desktop project we need to add `wix`'s licence to the Notice.txt even if it is not a first-level dependency. To do that define
all dependencies at `dependencies` array at the configuration. For any dependency at `dependencies` section, pipeline code will add their
licenses to the Notice.txt file.
At the webapp project, `webpack` is used in `devDependencies` and it is requested because we are using some files generated by `webpack`. If there is any dependency
referenced at `devDependencies` section, pipeline will add those dependency licences when they are referenced at `package.json` files.
Sample:
```
title: "Mattermost Mobile"
copyright: "© 2016-present Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information."
description: "This document includes a list of open source components used in Mattermost Mobile, including those that have been modified."
reviewers:
- mattermost/release-managers
- enahum
search:
- "package.json"
dependencies:
- "wix"
devDependencies:
- "webpack"
```
| Field | Type | Purpose |
| :-- | :-- | :-- |
| title | string | Field content will be used as a title of the application. See first line of `NOTICE.txt` file. |
| copyright | string | Field content will be used as a copyright message. See second line of `NOTICE.txt` file. |
| description | string | Field content will be used as notice file description. See third line of `NOTICE.txt` file. |
| reviewers | array of GitHub user/teams | Those will be automatically assigned to the PRs as reviewers. |
| dependencies | array | If any dependency name mentioned, it will be automatically added even if it is not a first-level dependency. |
| devDependencies | array | If any dependency name mentioned, it will be added when it is referenced in devDependency section. |
| search | array | Pipeline will search for package.json files mentioned here. Globstar format is supported ie. `packages/**/package.json`. |

View File

@@ -0,0 +1,14 @@
---
title: "Mattermost Mobile"
copyright: "© 2016-present Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information."
description: "This document includes a list of open source components used in Mattermost Mobile, including those that have been modified."
reviewers:
- "mattermost/release-managers"
- "enahum"
search:
- "package.json"
dependencies: []
devDependencies: []
...

View File

@@ -8,8 +8,8 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.590.0)
aws-sdk-core (3.131.1)
aws-partitions (1.602.0)
aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
@@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.1)
fastlane (2.207.0)
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.21.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.5.0)
google-apis-androidpublisher_v3 (0.23.0)
google-apis-core (>= 0.6, < 2.a)
google-apis-core (0.7.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -122,19 +122,19 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
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.14.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-iamcredentials_v1 (0.12.0)
google-apis-core (>= 0.6, < 2.a)
google-apis-playcustomapp_v1 (0.9.0)
google-apis-core (>= 0.6, < 2.a)
google-apis-storage_v1 (0.16.0)
google-apis-core (>= 0.6, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.2)
google-cloud-storage (1.37.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@@ -142,7 +142,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.3)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -150,12 +150,12 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
jwt (2.3.0)
jwt (2.4.1)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
@@ -183,9 +183,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.1)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@@ -203,11 +203,11 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.1)
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)

View File

@@ -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 = 410;
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 = 410;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.52.0</string>
<string>1.54.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>398</string>
<string>410</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.52.0</string>
<string>1.54.0</string>
<key>CFBundleVersion</key>
<string>398</string>
<string>410</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.52.0</string>
<string>1.54.0</string>
<key>CFBundleVersion</key>
<string>398</string>
<string>410</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -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
View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.52.0",
"version": "1.54.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.52.0",
"version": "1.54.0",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",

View File

@@ -168,7 +168,7 @@ declare module 'react-native-webrtc' {
}
export interface RTCPeerConnectionConfiguration {
iceServers: ConfigurationParamWithUrls[] | ConfigurationParamWithUrl[];
iceServers: Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
iceTransportPolicy?: 'all' | 'relay' | 'nohost' | 'none' | undefined;
bundlePolicy?: 'balanced' | 'max-compat' | 'max-bundle' | undefined;
rtcpMuxPolicy?: 'negotiate' | 'require' | undefined;