Compare commits

..

6 Commits

Author SHA1 Message Date
Mattermost Build
d965ef9e1e Bump app build number to 398 (#6236) (#6237)
(cherry picked from commit d5856f7b06)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2022-05-05 15:02:16 -04:00
Mattermost Build
b13ed0f409 MM-43904 - Fix: Calls batch actions (#6229) (#6232)
* fix batch actions

* tests

(cherry picked from commit 67c65156a7)

Co-authored-by: Christopher Poile <cpoile@gmail.com>
2022-05-05 14:13:18 -04:00
Mattermost Build
a046e70ba8 Bump app build number to 396 (#6218) (#6219)
(cherry picked from commit cd34873c23)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2022-05-03 18:32:32 -04:00
Mattermost Build
20d106b609 MM-43904 - Fix: Calls: "Access to route for non-existent plugin" error log (#6210) (#6212)
* add isCallsPluginEnabled; refactor Calls.PluginId

* revert Podfile.lock changes

(cherry picked from commit 912287fbe0)

Co-authored-by: Christopher Poile <cpoile@gmail.com>
2022-05-03 15:10:46 -04:00
Mattermost Build
b476fc00ff Bump version 1.52.0 build 394 (#6201) (#6202)
* Bump app version number to  1.52.0

* Bump app build number to  394

(cherry picked from commit c1e3c878e3)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2022-04-29 18:28:04 -04:00
Mattermost Build
80357fa57b Update NOTICE.txt (#6198) (#6199)
(cherry picked from commit fe5bd60cec)

Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2022-04-29 10:18:55 -04:00
42 changed files with 249 additions and 671 deletions

View File

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

View File

@@ -27,7 +27,6 @@ import {removeUserFromList} from '@mm-redux/utils/user_utils';
import {batchLoadCalls} from '@mmproducts/calls/store/actions/calls';
import {
handleCallStarted,
handleCallEnded,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallUserMuted,
@@ -474,8 +473,6 @@ 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,7 +63,6 @@ export default class DraftInput extends PureComponent {
channelMemberCountsByGroup: PropTypes.object,
groupsWithAllowReference: PropTypes.object,
addRecentUsedEmojisInMessage: PropTypes.func.isRequired,
endCallAlert: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -297,12 +296,6 @@ 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,7 +18,6 @@ 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';
@@ -104,7 +103,6 @@ const mapDispatchToProps = {
setStatus,
getChannelMemberCountsByGroup,
addRecentUsedEmojisInMessage,
endCallAlert,
};
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostDraft);

View File

@@ -12,8 +12,4 @@ const RequiredServer = {
const PluginId = 'com.mattermost.calls';
// Used for case when cloud server is using Calls v0.5.3.
// This can be removed when v0.5.4 is prepackaged in cloud.
const DefaultCloudMaxParticipants = 8;
export default {RequiredServer, RefreshConfigMillis, PluginId, DefaultCloudMaxParticipants};
export default {RequiredServer, RefreshConfigMillis, PluginId};

View File

@@ -65,7 +65,6 @@ 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,7 +9,6 @@ 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 {
@@ -52,13 +51,6 @@ const ClientCalls = (superclass: any) => class extends superclass {
{method: 'post', body: JSON.stringify({enabled: false})},
);
};
endCall = async (channelId: string) => {
return this.doFetch(
`${this.getCallsRoute()}/calls/${channelId}/end`,
{method: 'post'},
);
};
};
export default ClientCalls;

View File

@@ -57,40 +57,31 @@ exports[`CallMessage should match snapshot 1`] = `
<TouchableOpacity
onPress={[Function]}
style={
Array [
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#339970",
"borderRadius": 8,
"flexDirection": "row",
"padding": 12,
},
undefined,
]
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#339970",
"borderRadius": 8,
"flexDirection": "row",
"padding": 12,
}
}
>
<CompassIcon
name="phone-outline"
size={16}
style={
Array [
Object {
"color": "white",
"marginRight": 5,
},
undefined,
]
Object {
"color": "white",
"marginRight": 5,
}
}
/>
<Text
style={
Array [
Object {
"color": "white",
},
undefined,
]
Object {
"color": "white",
}
}
>
Join call
@@ -248,40 +239,31 @@ exports[`CallMessage should match snapshot for the call already in the current c
<TouchableOpacity
onPress={[Function]}
style={
Array [
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#339970",
"borderRadius": 8,
"flexDirection": "row",
"padding": 12,
},
undefined,
]
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#339970",
"borderRadius": 8,
"flexDirection": "row",
"padding": 12,
}
}
>
<CompassIcon
name="phone-outline"
size={16}
style={
Array [
Object {
"color": "white",
"marginRight": 5,
},
undefined,
]
Object {
"color": "white",
"marginRight": 5,
}
}
/>
<Text
style={
Array [
Object {
"color": "white",
},
undefined,
]
Object {
"color": "white",
}
}
>
Current call

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import moment from 'moment-timezone';
import React from 'react';
import React, {useCallback} from 'react';
import {injectIntl, intlShape, IntlShape} from 'react-intl';
import {View, TouchableOpacity, Text} from 'react-native';
@@ -32,7 +32,6 @@ type CallMessageProps = {
currentChannelName: string;
callChannelName: string;
intl: typeof IntlShape;
isLimitRestricted: boolean;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -67,16 +66,10 @@ 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',
@@ -89,9 +82,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
alignItems: 'center',
alignContent: 'center',
},
joinCallButtonRestricted: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
timeText: {
color: theme.centerChannelColor,
},
@@ -108,28 +98,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
const CallMessage = ({
post,
user,
teammateNameDisplay,
confirmToJoin,
alreadyInTheCall,
theme,
actions,
userTimezone,
isMilitaryTime,
currentChannelName,
callChannelName,
intl,
isLimitRestricted,
}: CallMessageProps) => {
const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInTheCall, theme, actions, userTimezone, isMilitaryTime, currentChannelName, callChannelName, intl}: CallMessageProps) => {
const style = getStyleSheet(theme);
const joinHandler = () => {
if (alreadyInTheCall || isLimitRestricted) {
const joinHandler = useCallback(() => {
if (alreadyInTheCall) {
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 (
@@ -166,8 +142,6 @@ const CallMessage = ({
);
}
const joinCallButtonText = alreadyInTheCall ? 'Current call' : 'Join call';
return (
<View style={style.messageStyle}>
<CompassIcon
@@ -187,17 +161,22 @@ const CallMessage = ({
</View>
<TouchableOpacity
style={[style.joinCallButton, isLimitRestricted && style.joinCallButtonRestricted]}
style={style.joinCallButton}
onPress={joinHandler}
>
<CompassIcon
name='phone-outline'
size={16}
style={[style.joinCallButtonIcon, isLimitRestricted && style.joinCallButtonIconRestricted]}
style={style.joinCallButtonIcon}
/>
<Text style={[style.joinCallButtonText, isLimitRestricted && style.joinCallButtonTextRestricted]}>
{joinCallButtonText}
</Text>
{alreadyInTheCall &&
<Text
style={style.joinCallButtonText}
>{'Current call'}</Text>}
{!alreadyInTheCall &&
<Text
style={style.joinCallButtonText}
>{'Join call'}</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, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
import CallMessage from './call_message';
@@ -41,7 +41,6 @@ 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, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
import {getCalls, getCurrentCall} 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,7 +28,6 @@ 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);
@@ -40,7 +39,7 @@ const StartCall = ({testID, theme, intl, joinCall}: Props) => {
const [tryLeaveAndJoin, msgPostfix] = useTryCallsFunction(leaveAndJoin);
const handleStartCall = useCallback(preventDoubleTap(tryLeaveAndJoin), [tryLeaveAndJoin]);
if (alreadyInTheCall || limitRestricted) {
if (alreadyInTheCall) {
return null;
}

View File

@@ -1,100 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JoinCall should match snapshot 1`] = `
<View
<Pressable
onPress={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"alignItems": "center",
"backgroundColor": "#3DB887",
"flexDirection": "row",
"height": 38,
"justifyContent": "center",
"padding": 5,
"width": "100%",
}
}
>
<Pressable
onPress={[Function]}
<CompassIcon
name="phone-in-talk"
size={16}
style={
Array [
Object {
"alignItems": "center",
"backgroundColor": "#3DB887",
"flexDirection": "row",
"height": 38,
"justifyContent": "center",
"padding": 5,
"width": "100%",
},
false,
]
Object {
"color": "#ffffff",
"marginLeft": 10,
"marginRight": 5,
}
}
/>
<Text
style={
Object {
"color": "#ffffff",
"fontSize": 16,
"fontWeight": "bold",
}
}
>
<CompassIcon
name="phone-in-talk"
size={16}
style={
Object {
"color": "#ffffff",
"marginLeft": 10,
"marginRight": 5,
}
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",
]
}
/>
<Text
style={
Object {
"color": "#ffffff",
"fontSize": 16,
"fontWeight": "bold",
}
}
>
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>
</View>
</View>
</Pressable>
`;

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, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls';
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
import JoinCall from './join_call';
@@ -23,7 +23,6 @@ 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,7 +38,6 @@ describe('JoinCall', () => {
alreadyInTheCall: false,
currentChannelName: 'Current Channel',
callChannelName: 'Call Channel',
isLimitRestricted: false,
};
test('should match snapshot', () => {
@@ -57,7 +56,7 @@ describe('JoinCall', () => {
test('should join on click', () => {
const joinCall = jest.fn();
const props = {...baseProps, actions: {joinCall}};
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive().childAt(0);
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
wrapper.simulate('press');
expect(Alert.alert).not.toHaveBeenCalled();
@@ -67,7 +66,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().childAt(0);
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
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, {useEffect, useMemo} from 'react';
import React, {useCallback, useEffect, useMemo} from 'react';
import {injectIntl, IntlShape} from 'react-intl';
import {View, Text, Pressable} from 'react-native';
@@ -26,62 +26,49 @@ type Props = {
alreadyInTheCall: boolean;
currentChannelName: string;
callChannelName: string;
isLimitRestricted: boolean;
intl: typeof IntlShape;
}
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 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 JoinCall = (props: Props) => {
if (!props.call) {
@@ -95,12 +82,9 @@ const JoinCall = (props: Props) => {
};
}, [props.call, props.alreadyInTheCall]);
const joinHandler = () => {
if (props.isLimitRestricted) {
return;
}
const joinHandler = useCallback(() => {
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;
@@ -112,39 +96,32 @@ const JoinCall = (props: Props) => {
}, [props.call.participants]);
return (
<View style={style.outerContainer}>
<Pressable
style={[style.innerContainer, props.isLimitRestricted && style.innerContainerRestricted]}
onPress={joinHandler}
>
<CompassIcon
name='phone-in-talk'
size={16}
style={style.joinCallIcon}
<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}
/>
<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>
</Text>
<View style={style.avatars}>
<Avatars
userIds={userIds}
breakAt={1}
listTitle={
<Text style={style.headerText}>{'Call participants'}</Text>
}
/>
</View>
</Pressable>
);
};
export default injectIntl(JoinCall);

View File

@@ -4,7 +4,6 @@
// 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,
@@ -13,7 +12,6 @@ import {
} from 'react-native-webrtc';
import {Client4} from '@client/rest';
import {WebsocketEvents} from '@constants';
import Peer from './simple-peer';
import WebSocketClient from './websocket';
@@ -22,13 +20,12 @@ export let client: any = null;
const websocketConnectTimeout = 3000;
export async function newClient(channelID: string, iceServers: string[], closeCb: () => void, setScreenShareURL: (url: string) => void) {
export async function newClient(channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
let peer: Peer | null = null;
let stream: MediaStream;
let voiceTrackAdded = false;
let voiceTrack: MediaStreamTrack | null = null;
let isClosed = false;
let onCallEnd: EmitterSubscription | null = null;
const streams: MediaStream[] = [];
try {
@@ -50,11 +47,6 @@ export async function newClient(channelID: string, iceServers: string[], closeCb
ws.close();
}
if (onCallEnd) {
onCallEnd.remove();
onCallEnd = null;
}
streams.forEach((s) => {
s.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
@@ -73,12 +65,6 @@ export async function newClient(channelID: string, iceServers: string[], closeCb
}
};
onCallEnd = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_CALL_END, ({channelId}) => {
if (channelId === channelID) {
disconnect();
}
});
const mute = () => {
if (!peer) {
return;
@@ -133,8 +119,16 @@ export async function newClient(channelID: string, iceServers: string[], closeCb
});
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, iceServers);
peer = new Peer(null, config.ICEServers);
peer.on('signal', (data: any) => {
if (data.type === 'offer' || data.type === 'answer') {
ws.send('sdp', {

View File

@@ -323,16 +323,6 @@ 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

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

@@ -117,14 +117,11 @@ 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,7 +2,6 @@
// 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';
@@ -10,10 +9,6 @@ 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,
@@ -22,16 +17,10 @@ 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 {
getCallInCurrentChannel,
getConfig,
getNumCurrentConnectedParticipants,
} from '@mmproducts/calls/store/selectors/calls';
import {getConfig} 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;
@@ -93,7 +82,6 @@ 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;
@@ -201,10 +189,7 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc
dispatch(setSpeakerphoneOn(false));
try {
ws = await newClient(channelId, getConfig(getState()).ICEServers, () => {
dispatch(setSpeakerphoneOn(false));
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
}, setScreenShareURL);
ws = await newClient(channelId, () => null, setScreenShareURL);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
@@ -227,11 +212,13 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc
}
export function leaveCall(): ActionFunc {
return async () => {
return async (dispatch: DispatchFunc) => {
if (ws) {
ws.disconnect();
ws = null;
}
dispatch(setSpeakerphoneOn(false));
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
return {};
};
}
@@ -273,51 +260,3 @@ 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,15 +50,7 @@ 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: {}, 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},
data: {channelId: msg.data.channelID, startTime: msg.data.start_at, threadId: msg.data.thread_id, participants: {}},
};
}

View File

@@ -145,16 +145,6 @@ 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 = {

View File

@@ -53,11 +53,6 @@ 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};
@@ -167,12 +162,6 @@ 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,12 +4,11 @@
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', participants: [{id: 'me'}]};
const call1 = {id: 'call1'};
const call2 = {id: 'call2'};
const testState = deepFreezeAndThrowOnMutation({
entities: {
@@ -21,10 +20,6 @@ describe('Selectors.Calls', () => {
joined: 'call1',
enabled: {'channel-1': true, 'channel-2': false},
screenShareURL: 'screenshare-url',
config: DefaultServerConfig,
},
general: {
license: {},
},
},
});
@@ -82,119 +77,4 @@ 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);
});
});

View File

@@ -1,15 +1,12 @@
// 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 {getLicense, getServerVersion} from '@mm-redux/selectors/entities/general';
import {getServerVersion} from '@mm-redux/selectors/entities/general';
import {GlobalState} from '@mm-redux/types/store';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {Call} from '@mmproducts/calls/store/types/calls';
export function getConfig(state: GlobalState) {
return state.entities.calls.config;
@@ -68,49 +65,3 @@ 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;
},
);

View File

@@ -15,13 +15,12 @@ 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 = {
@@ -50,7 +49,6 @@ export type ServerCallState = {
states: ServerUserState[];
thread_id: string;
screen_sharing_id: string;
creator_id: string;
}
export type VoiceEventData = {
@@ -62,8 +60,6 @@ export type ServerConfig = {
ICEServers: string[];
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
MaxCallParticipants: number;
sku_short_name: string;
last_retrieved_at: number;
}
@@ -71,7 +67,5 @@ export const DefaultServerConfig = {
ICEServers: [],
AllowEnableCalls: false,
DefaultEnabled: false,
MaxCallParticipants: 0,
sku_short_name: '',
last_retrieved_at: 0,
} as ServerConfig;

View File

@@ -48,14 +48,3 @@ 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

@@ -11,12 +11,9 @@
"about.teamEditiont1": "Корпоративна версия",
"about.title": "Относно {appTitle}",
"announcment_banner.dont_show_again": "Не показвай отново",
"api.channel.add_guest.added": "{addedUsername} е добавен към канала като гост от {username}.",
"api.channel.add_member.added": "{addedUsername} е добавен в канала от {username}.",
"api.channel.guest_join_channel.post_and_forget": "{username} се присъедини към канала като гост.",
"apps.error": "Грешка: {error}",
"apps.error.command.field_missing": "Липсват задължителни полета: `{fieldName}`.",
"apps.error.command.same_channel": "Повтаря се каналът с поле `{fieldName}`: `{option}`.",
"apps.error.command.unknown_channel": "Неизвестен канал за полето `{fieldName}`: `{option}`.",
"apps.error.command.unknown_option": "Неизвестна опция за полето `{fieldName}`: `{option}`.",
"apps.error.command.unknown_user": "Неизвестен потребител за поле `{fieldName}`: `{option}`.",

View File

@@ -30,7 +30,7 @@
"apps.error.form.refresh_no_refresh": "Frissítést hívott egy frissítés nélküli mezőn.",
"apps.error.form.submit.pretext": "Hiba történt az adatok elküldése során. Lépjen kapcsolatba az alkalmazás fejlesztőjével. Részletek: {details}",
"apps.error.lookup.error_preparing_request": "Hiba a keresési kérelem előkészítése során: {errorMessage}",
"apps.error.malformed_binding": "Ez a kapcsolódás nincs jól formázva. Lépjen kapcsolatba az App fejlesztőjével.",
"apps.error.malformed_binding": "Ez az összekapcsolás nincs jól formázva. Lépjen kapcsolatba az App fejlesztőjével.",
"apps.error.parser": "Feldolgozási hiba: {error}",
"apps.error.parser.empty_value": "üres értékek nem megengedettek",
"apps.error.parser.execute_non_leaf": "Ki kell választania egy alparancsot.",
@@ -39,7 +39,7 @@
"apps.error.parser.missing_list_end": "Az elvárt a lista záró token.",
"apps.error.parser.missing_quote": "Egyező idézőjel elvárt a bemenet vége előtt.",
"apps.error.parser.missing_source": "Az űrlapnak nincs sem submit sem source mezője.",
"apps.error.parser.missing_submit": "Nincs submit meghívás az összekötőben vagy az űrlapban.",
"apps.error.parser.missing_submit": "Nincs submit meghívás az összekapcsolásban vagy a formon.",
"apps.error.parser.missing_tick": "Azonos idézőjelel kell használni a bemenet vége előtt.",
"apps.error.parser.multiple_equal": "Többszörös `=` jel nem engedélyezett.",
"apps.error.parser.no_argument_pos_x": "Nem sikerült azonosítani az argumentumot.",

View File

@@ -16,49 +16,35 @@
"api.channel.guest_join_channel.post_and_forget": "{username}이(가) 게스트로 채널에 참여했습니다.",
"apps.error": "오류: {error}",
"apps.error.command.field_missing": "필수 필드 누락: `{fieldName}`.",
"apps.error.command.same_channel": "필드 `{fieldName}`에서 채널이 반복 지정되었습니다: `{option}`.",
"apps.error.command.same_option": "필드 `{fieldName}` 에서 옵션이 반복 지정되었습니다: `{option}`.",
"apps.error.command.same_user": "필드 `{fieldName}` 에서 사용자가 반복 지정되었습니다: `{option}`.",
"apps.error.command.unknown_channel": "`{fieldName}` 필드에 대한 알 수 없는 채널: `{option}`.",
"apps.error.command.unknown_option": "`{fieldName}` 필드에 대한 알 수 없는 옵션: `{option}`.",
"apps.error.command.unknown_user": "`{fieldName}` 필드의 알 수 없는 사용자: `{option}`.",
"apps.error.form.no_form": "`form`이 정의되어 있지 않습니다.",
"apps.error.form.no_lookup": "`lookup` 이 정의되어 있지 않습니다.",
"apps.error.form.no_source": "`source` 가 정의되어 있지 않습니다.",
"apps.error.form.no_submit": "`submit` 가 정의되어 있지 않습니다",
"apps.error.form.refresh": "선택한 필드를 가져오는 동안 오류가 발생했습니다. 앱 개발자에게 문의하세요. 세부정보: {details}",
"apps.error.form.refresh_no_refresh": "새로고침이 불가능한 필드에 대해 새로고침을 실행합니다.",
"apps.error.form.submit.pretext": "모달을 제출하는 중에 오류가 발생했습니다. 앱 개발자에게 문의하세요. 세부정보: {details}",
"apps.error.lookup.error_preparing_request": "조회 요청을 준비하는 중 오류 발생: {errorMessage}",
"apps.error.malformed_binding": "바인딩이 제대로 형성되지 않았습니다. 앱 개발자에게 문의하십시오.",
"apps.error.parser": "파싱 오류: {error}",
"apps.error.parser.empty_value": "빈 값은 허용되지 않습니다",
"apps.error.parser.execute_non_leaf": "하위 명령을 선택해야 합니다.",
"apps.error.parser.missing_binding": "커맨드 연결을 찾을 수 없습니다.",
"apps.error.parser.missing_field_value": "필드 값이 누락되었습니다.",
"apps.error.parser.missing_list_end": "목록을 닫는 토큰이 필요합니다.",
"apps.error.parser.missing_quote": "입력 종료 전에 \"가 필요합니다.",
"apps.error.parser.missing_source": "양식이 제출되지 않았거나 원본이 없습니다.",
"apps.error.parser.missing_submit": "바인딩 또는 양식에 제출 호출이 없습니다.",
"apps.error.parser.missing_tick": "입력 종료 전에 '가 필요합니다.",
"apps.error.parser.multiple_equal": "'=' 기호는 여러 개 사용할 수 없습니다.",
"apps.error.parser.no_argument_pos_x": "인수를 식별할 수 없습니다.",
"apps.error.parser.no_bindings": "바인딩 된 명령어가 없습니다.",
"apps.error.parser.no_form": "양식을 찾을 수 없습니다.",
"apps.error.parser.no_match": "`{command}`: 이 작업 공간에서 일치하는 명령어를 찾을 수 없습니다.",
"apps.error.parser.no_slash_start": "명령어는 '/'로 시작해야 합니다.",
"apps.error.parser.unexpected_character": "예기치 않은 문자.",
"apps.error.parser.unexpected_comma": "예기치 않은 콤마.",
"apps.error.parser.unexpected_error": "예기치 않은 오류입니다.",
"apps.error.parser.unexpected_flag": "명령어에서`{flagName}`플래그를 허용하지 않습니다.",
"apps.error.parser.unexpected_squared_bracket": "예기치 않은 목록이 열립니다.",
"apps.error.parser.unexpected_state": "연결할 수 없음: matchBinding에서 예기치 않은 상태: `{state}`.",
"apps.error.parser.unexpected_flag": "명령어에서 '{flagName}' 플래그를 허용하지 않습니다.",
"apps.error.parser.unexpected_state": "연결할 수 없음: matchBinding에서 예기치 않은 상태: '{state}'.",
"apps.error.parser.unexpected_whitespace": "연결할 수 없음: 예기치 않은 공백입니다.",
"apps.error.responses.form.no_form": "응답 유형은 `form`이지만 응답에 `form`이 포함되지 않았습니다.",
"apps.error.responses.navigate.no_url": "응답 유형은 `navigate`이지만, URL이 포함되지 않았습니다.",
"apps.error.responses.unexpected_error": "예기치 않은 오류를 수신했습니다.",
"apps.error.responses.unexpected_type": "예상하지 못한 앱 응답 유형입니다. 응답 유형: {type}.",
"apps.error.responses.unknown_field_error": "알 수 없는 필드에 대한 오류를 수신했습니다. 필드 이름: `{field}`. 오류: `{error}`.",
"apps.error.responses.unknown_field_error": "알 수 없는 필드에 대한 오류를 수신했습니다. 필드 이름: '{field}'. 오류: '{error}'.",
"apps.error.responses.unknown_type": "지원되지 않는 앱 응답 유형입니다. 응답 유형: {type}.",
"apps.error.unknown": "알 수 없는 오류가 발생했습니다.",
"apps.suggestion.dynamic.error": "동적 선택 오류",
@@ -415,8 +401,6 @@
"mobile.message_length.message": "Your current message is too long. Current character count: {max}/{count}",
"mobile.message_length.message_split_left": "메시지가 문자 제한을 초과합니다",
"mobile.message_length.title": "Message Length",
"mobile.microphone_permission_denied_description": "이 호출에 참여하려면 마이크로폰에 대한 가장 중요한 액세스 권한을 부여하기 위한 설정을 여십시오.",
"mobile.microphone_permission_denied_title": "{applicationName}이(가) 마이크에 액세스하려고 합니다",
"mobile.more_dms.add_more": "You can add {remaining, number} more users",
"mobile.more_dms.cannot_add_more": "You cannot add more users",
"mobile.more_dms.one_more": "You can add 1 more user",

View File

@@ -686,7 +686,7 @@
"post_body.check_for_out_of_channel_groups_mentions.message": "werd niet verwittigd door deze vermelding omdat deze niet in het kanaal zijn. Zij kunnen niet aan het kanaal toegevoegd worden omdat ze geen lid zijn van de gekoppelde groepen. Om hem aan het kanaal toe te voegen, moeten ze toegevoegd worden aan de gelinkte groepen.",
"post_body.check_for_out_of_channel_mentions.link.and": " en ",
"post_body.check_for_out_of_channel_mentions.link.private": "voeg ze toe aan dit privé-kanaal",
"post_body.check_for_out_of_channel_mentions.link.public": "ze toevoegen aan het kanaal",
"post_body.check_for_out_of_channel_mentions.link.public": "voeg ze toe aan het kanaal",
"post_body.check_for_out_of_channel_mentions.message.multiple": "niet door deze vermelding op de hoogte zijn gebracht omdat ze niet in het kanaal aanwezig zijn. Wil je ",
"post_body.check_for_out_of_channel_mentions.message.one": "niet door deze vermelding op de hoogte zijn gebracht omdat ze niet in het kanaal aanwezig zijn. Wil je ",
"post_body.check_for_out_of_channel_mentions.message_last": "? Ze zullen toegang hebben tot de volledige berichtgeschiedenis.",

View File

@@ -415,8 +415,6 @@
"mobile.message_length.message": "Ditt meddelande är för långt. Nuvarande antal tecken: {max}/{count}",
"mobile.message_length.message_split_left": "Meddelandet överskrider teckenbegränsningen",
"mobile.message_length.title": "Meddelandelängd",
"mobile.microphone_permission_denied_description": "För att delta i samtalet, öppna Inställningar och ge Mattermost tillåtelse att använda mikrofonen.",
"mobile.microphone_permission_denied_title": "{applicationName} behöver åtkomst till din mikrofon",
"mobile.more_dms.add_more": "Du kan lägga till {remaining, number} användare",
"mobile.more_dms.cannot_add_more": "Du kan lägga till 1 till användare",
"mobile.more_dms.one_more": "Du kan lägga till 1 till användare",
@@ -703,7 +701,7 @@
"rhs_thread.rootPostDeletedMessage.body": "Delar av denna tråd har raderats utifrån gallringsregler. Det går inte att svara i denna tråd.",
"search_bar.search": "Sök",
"search_header.results": "Sökresultat",
"search_header.title2": "Senaste omnämnanden",
"search_header.title2": "Nyliga omnämnanden",
"search_header.title3": "Sparade meddelanden",
"search_item.channelArchived": "Arkiverad",
"sidebar.channels": "PUBLIKA KANALER",

View File

@@ -415,8 +415,6 @@
"mobile.message_length.message": "您的消息过长。目前字数:{max}/{count}",
"mobile.message_length.message_split_left": "消息超过字数限制",
"mobile.message_length.title": "消息长度",
"mobile.microphone_permission_denied_description": "若要参与此呼叫,请打开“设置”以授予对您的麦克风“授权使用”的权限。",
"mobile.microphone_permission_denied_title": "{applicationName} 想使用您的麦克风",
"mobile.more_dms.add_more": "您还可以添加 {remaining, number} 位用户",
"mobile.more_dms.cannot_add_more": "您不能再添加更多用户",
"mobile.more_dms.one_more": "您还能添加 1 个用户",

View File

@@ -12,7 +12,6 @@
"about.title": "關於 {appTitle}",
"announcment_banner.dont_show_again": "不要再顯示",
"api.channel.add_member.added": "{username} 已將 {addedUsername} 加入頻道",
"apps.error": "錯誤:{error}",
"archivedChannelMessage": "正在觀看**被封存的頻道**。不能發布新訊息。",
"center_panel.archived.closeChannel": "關閉頻道",
"channel.channelHasGuests": "此頻道有訪客",

View File

@@ -8,16 +8,16 @@ 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.581.0)
aws-sdk-core (3.130.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.57.0)
jmespath (~> 1.0)
aws-sdk-kms (1.56.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-s3 (1.113.2)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.206.1)
fastlane (2.205.2)
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-androidpublisher_v3 (0.19.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.5.0)
google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -126,7 +126,7 @@ GEM
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.14.0)
google-apis-storage_v1 (0.13.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
@@ -154,7 +154,7 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.11.0)
@@ -164,7 +164,7 @@ GEM
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
nokogiri (1.13.6)
nokogiri (1.13.4)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
optparse (0.1.1)
@@ -173,7 +173,7 @@ GEM
public_suffix (4.0.7)
racc (1.6.0)
rake (13.0.6)
representable (3.2.0)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.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 = 404;
CURRENT_PROJECT_VERSION = 398;
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 = 404;
CURRENT_PROJECT_VERSION = 398;
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.53.0</string>
<string>1.52.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>404</string>
<string>398</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.53.0</string>
<string>1.52.0</string>
<key>CFBundleVersion</key>
<string>404</string>
<string>398</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.53.0</string>
<string>1.52.0</string>
<key>CFBundleVersion</key>
<string>404</string>
<string>398</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -733,7 +733,7 @@ SPEC CHECKSUMS:
FBLazyVector: 244195e30d63d7f564c55da4410b9a24e8fbceaa
FBReactNativeSpec: c94002c1d93da3658f4d5119c6994d19961e3d52
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
glog: 5337263514dd6f09803962437687240c5dc39aa4
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba
jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073
@@ -746,7 +746,7 @@ SPEC CHECKSUMS:
Permission-Notifications: bb420c3d28328df24de1b476b41ed8249ccf2537
Permission-PhotoLibrary: 7bec836dcdd04a0bfb200c314f1aae06d4476357
Permission-PhotoLibraryAddOnly: 06fb0cdb1d35683b235ad8c464ef0ecc88859ea3
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
RCTRequired: cd47794163052d2b8318c891a7a14fcfaccc75ab
RCTTypeSafety: 393bb40b3e357b224cde53d3fec26813c52428b1
RCTYouTube: a8bb45705622a6fc9decf64be04128d3658ed411
@@ -824,4 +824,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 8214414d5676358401d8ad51dff19e7fd8c71b5c
COCOAPODS: 1.11.3
COCOAPODS: 1.11.2

2
package-lock.json generated
View File

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

View File

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