Merge branch 'main' into MM-50324_Increase-nickname-character-limit

This commit is contained in:
Mattermost Build 2023-03-29 17:51:00 +03:00 committed by GitHub
commit 11226c5620
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 808 additions and 709 deletions

View file

@ -110,7 +110,7 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 461
versionCode 463
versionName "2.1.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -8,6 +8,7 @@ import {t} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
@ -63,10 +64,12 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou
}
if (!response.ok) {
logDebug('Server ping returned not ok response', response);
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}
} catch (error) {
logDebug('Server ping threw an exception', error);
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}

View file

@ -26,7 +26,7 @@ import {queryAllUsers} from '@queries/servers/user';
import {setFetchingThreadState} from '@store/fetching_thread_store';
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
import {isServerError} from '@utils/errors';
import {logError} from '@utils/log';
import {logDebug, logError} from '@utils/log';
import {processPostsFetched} from '@utils/post';
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
@ -136,6 +136,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
try {
created = await client.createPost(newPost);
} catch (error) {
logDebug('Error sending a post', error);
const errorPost = {
...newPost,
id: pendingPostId,

View file

@ -5,6 +5,7 @@ import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
import {openAsBottomSheet} from '@screens/navigation';
@ -23,6 +24,8 @@ type HeaderDisplayNameProps = {
userIconOverride?: string;
userId: string;
usernameOverride?: string;
showCustomStatusEmoji: boolean;
customStatus: UserCustomStatus;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -30,11 +33,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
displayName: {
color: theme.centerChannelColor,
flexGrow: 1,
marginRight: 5,
...typography('Body', 200, 'SemiBold'),
},
displayNameCustomEmojiWidth: {
maxWidth: '90%',
},
displayNameContainer: {
maxWidth: '60%',
marginRight: 5,
flexDirection: 'row',
alignItems: 'center',
},
displayNameContainerBotReplyWidth: {
maxWidth: '50%',
@ -45,7 +53,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
displayNameContainerLandscapeBotReplyWidth: {
maxWidth: '70%',
},
customStatusEmoji: {
color: theme.centerChannelColor,
marginRight: 4,
},
};
});
@ -54,6 +65,7 @@ const HeaderDisplayName = ({
location, rootPostAuthor,
shouldRenderReplyButton, theme,
userIconOverride, userId, usernameOverride,
showCustomStatusEmoji, customStatus,
}: HeaderDisplayNameProps) => {
const dimensions = useWindowDimensions();
const intl = useIntl();
@ -85,12 +97,17 @@ const HeaderDisplayName = ({
};
const displayNameWidth = calcNameWidth();
const displayNameStyle = [style.displayNameContainer, displayNameWidth];
const displayNameContainerStyle = [style.displayNameContainer, displayNameWidth];
const displayNameStyle = showCustomStatusEmoji ? style.displayNameCustomEmojiWidth : null;
if (displayName) {
return (
<View style={displayNameStyle}>
<TouchableOpacity onPress={onPress}>
<TouchableOpacity
style={displayNameContainerStyle}
onPress={onPress}
>
<View style={displayNameStyle}>
<Text
style={style.displayName}
ellipsizeMode={'tail'}
@ -99,8 +116,15 @@ const HeaderDisplayName = ({
>
{displayName}
</Text>
</TouchableOpacity>
</View>
</View>
{showCustomStatusEmoji && (
<CustomStatusEmoji
customStatus={customStatus!}
style={[style.customStatusEmoji]}
testID='post_header'
/>
)}
</TouchableOpacity>
);
}

View file

@ -4,7 +4,6 @@
import React from 'react';
import {View} from 'react-native';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import FormattedTime from '@components/formatted_time';
import PostPriorityLabel from '@components/post_priority/post_priority_label';
import {CHANNEL, THREAD} from '@constants/screens';
@ -67,11 +66,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
alignSelf: 'center',
marginLeft: 6,
},
customStatusEmoji: {
color: theme.centerChannelColor,
marginRight: 4,
marginTop: 2,
},
};
});
@ -89,11 +83,10 @@ const Header = (props: HeaderProps) => {
const displayName = postUserDisplayName(post, author, teammateNameDisplay, enablePostUsernameOverride);
const rootAuthorDisplayName = rootPostAuthor ? displayUsername(rootPostAuthor, currentUser.locale, teammateNameDisplay, true) : undefined;
const customStatus = getUserCustomStatus(author);
const customStatusExpired = isCustomStatusExpired(author);
const showCustomStatusEmoji = Boolean(
isCustomStatusEnabled && displayName && customStatus &&
!(isSystemPost || author?.isBot || isAutoResponse || isWebHook),
);
) && !isCustomStatusExpired(author) && Boolean(customStatus?.emoji);
return (
<>
@ -110,14 +103,9 @@ const Header = (props: HeaderProps) => {
userIconOverride={post.props?.override_icon_url}
userId={post.userId}
usernameOverride={post.props?.override_username}
showCustomStatusEmoji={showCustomStatusEmoji}
customStatus={customStatus!}
/>
{showCustomStatusEmoji && !customStatusExpired && Boolean(customStatus?.emoji) && (
<CustomStatusEmoji
customStatus={customStatus!}
style={style.customStatusEmoji}
testID='post_header'
/>
)}
{(!isSystemPost || isAutoResponse) &&
<HeaderTag
isAutoResponder={isAutoResponse}

View file

@ -43,14 +43,13 @@ import type {
ApiResp,
Call,
CallParticipant,
CallReactionEmoji,
CallsConnection,
RecordingState,
ServerCallState,
ServerChannelState,
} from '@calls/types/calls';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {CallRecordingState, EmojiData} from '@mattermost/calls/lib/types';
import type {IntlShape} from 'react-intl';
let connection: CallsConnection | null = null;
@ -279,7 +278,6 @@ export const leaveCall = () => {
connection.disconnect();
connection = null;
}
setSpeakerphoneOn(false);
};
export const leaveCallPopCallScreen = async () => {
@ -322,7 +320,7 @@ export const unraiseHand = () => {
}
};
export const sendReaction = (emoji: CallReactionEmoji) => {
export const sendReaction = (emoji: EmojiData) => {
if (connection) {
connection.sendReaction(emoji);
}
@ -415,7 +413,7 @@ export const startCallRecording = async (serverUrl: string, callId: string) => {
const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState;
let data: ApiResp | CallRecordingState;
try {
data = await client.startCallRecording(callId);
} catch (error) {
@ -433,7 +431,7 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => {
const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState;
let data: ApiResp | CallRecordingState;
try {
data = await client.stopCallRecording(callId);
} catch (error) {

View file

@ -1,24 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {
ServerChannelState,
ServerCallsConfig,
ApiResp,
RecordingState,
} from '@calls/types/calls';
import type {ServerChannelState, ApiResp} from '@calls/types/calls';
import type {CallRecordingState, CallsConfig} from '@mattermost/calls/lib/types';
import type {RTCIceServer} from 'react-native-webrtc';
export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>;
getCalls: () => Promise<ServerChannelState[]>;
getCallForChannel: (channelId: string) => Promise<ServerChannelState>;
getCallsConfig: () => Promise<ServerCallsConfig>;
getCallsConfig: () => Promise<CallsConfig>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<RTCIceServer[]>;
startCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
stopCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
startCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
stopCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
}
const ClientCalls = (superclass: any) => class extends superclass {
@ -52,7 +48,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getCallsRoute()}/config`,
{method: 'get'},
) as ServerCallsConfig;
) as CallsConfig;
};
enableChannelCalls = async (channelId: string, enable: boolean) => {

View file

@ -8,7 +8,7 @@ import CompassIcon from '@components/compass_icon';
import Emoji from '@components/emoji';
import ProfilePicture from '@components/profile_picture';
import type {CallReactionEmoji} from '@calls/types/calls';
import type {EmojiData} from '@mattermost/calls/lib/types';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
@ -18,7 +18,7 @@ type Props = {
muted?: boolean;
sharingScreen?: boolean;
raisedHand?: boolean;
reaction?: CallReactionEmoji;
reaction?: EmojiData;
size?: 'm' | 'l';
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Pressable, StyleSheet, View} from 'react-native';
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native';
import {raiseHand, unraiseHand} from '@calls/actions';
import {sendReaction} from '@calls/actions/calls';
@ -16,12 +16,15 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
backgroundColor: 'rgba(255,255,255,0.16)',
width: '100%',
height: 64,
paddingLeft: 16,
paddingRight: 16,
},
containerInLandscape: {
paddingBottom: 6,
justifyContent: 'center',
},
button: {
display: 'flex',
flexDirection: 'row',
@ -33,6 +36,10 @@ const styles = StyleSheet.create({
paddingLeft: 10,
paddingRight: 10,
},
buttonLandscape: {
marginRight: 12,
marginLeft: 12,
},
buttonPressed: {
backgroundColor: 'rgba(245, 171, 0, 0.24)',
},
@ -54,6 +61,9 @@ interface Props {
}
const ReactionBar = ({raisedHand}: Props) => {
const {width, height} = useWindowDimensions();
const isLandscape = width > height;
const LowerHandText = (
<FormattedText
id={'mobile.calls_lower_hand'}
@ -77,9 +87,9 @@ const ReactionBar = ({raisedHand}: Props) => {
}, [raisedHand]);
return (
<View style={styles.container}>
<View style={[styles.container, isLandscape && styles.containerInLandscape]}>
<Pressable
style={[styles.button, Boolean(raisedHand) && styles.buttonPressed]}
style={[styles.button, isLandscape && styles.buttonLandscape, Boolean(raisedHand) && styles.buttonPressed]}
onPress={toggleRaiseHand}
>
<CompassIcon
@ -94,7 +104,7 @@ const ReactionBar = ({raisedHand}: Props) => {
<EmojiButton
key={name}
emojiName={name}
style={styles.button}
style={[styles.button, isLandscape && styles.buttonLandscape]}
onPress={() => sendReaction({name, unified})}
/>
))

View file

@ -1,28 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import {deflate} from 'pako/lib/deflate.js';
import {RTCPeer} from '@mattermost/calls/lib';
import {deflate} from 'pako';
import {DeviceEventEmitter, EmitterSubscription} from 'react-native';
import InCallManager from 'react-native-incall-manager';
import {
MediaStream,
MediaStreamTrack,
mediaDevices,
RTCPeerConnection,
} from 'react-native-webrtc';
import RTCPeer from '@calls/rtcpeer';
import {setSpeakerPhone} from '@calls/state';
import {getICEServersConfigs} from '@calls/utils';
import {WebsocketEvents} from '@constants';
import {getServerCredentials} from '@init/credentials';
import NetworkManager from '@managers/network_manager';
import {logError, logDebug, logWarning} from '@utils/log';
import {logError, logDebug, logWarning, logInfo} from '@utils/log';
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
import type {CallReactionEmoji, CallsConnection} from '@calls/types/calls';
import type {CallsConnection} from '@calls/types/calls';
import type {EmojiData} from '@mattermost/calls/lib/types';
const peerConnectTimeout = 5000;
@ -164,7 +164,7 @@ export async function newConnection(
}
};
const sendReaction = (emoji: CallReactionEmoji) => {
const sendReaction = (emoji: EmojiData) => {
if (ws) {
ws.send('react', {
data: JSON.stringify(emoji),
@ -204,7 +204,19 @@ export async function newConnection(
InCallManager.start({media: 'video'});
setSpeakerPhone(true);
peer = new RTCPeer({iceServers: iceConfigs || []});
peer = new RTCPeer({
iceServers: iceConfigs || [],
logger: {
logDebug,
logErr: logError,
logWarn: logWarning,
logInfo,
},
webrtc: {
MediaStream,
RTCPeerConnection,
},
});
peer.on('offer', (sdp) => {
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);

View file

@ -22,34 +22,43 @@ import {
import {WebsocketEvents} from '@constants';
import DatabaseManager from '@database/manager';
export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage) => {
import type {
CallHostChangedData, CallRecordingStateData,
CallStartData,
EmptyData,
UserConnectedData,
UserDisconnectedData, UserMutedUnmutedData, UserRaiseUnraiseHandData,
UserReactionData, UserScreenOnOffData, UserVoiceOnOffData,
} from '@mattermost/calls/lib/types';
export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage<UserConnectedData>) => {
// Load user model async (if needed).
fetchUsersByIds(serverUrl, [msg.data.userID]);
userJoinedCall(serverUrl, msg.broadcast.channel_id, msg.data.userID);
};
export const handleCallUserDisconnected = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserDisconnected = (serverUrl: string, msg: WebSocketMessage<UserDisconnectedData>) => {
userLeftCall(serverUrl, msg.broadcast.channel_id, msg.data.userID);
};
export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage<UserMutedUnmutedData>) => {
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, true);
};
export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage<UserMutedUnmutedData>) => {
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, false);
};
export const handleCallUserVoiceOn = (msg: WebSocketMessage) => {
export const handleCallUserVoiceOn = (msg: WebSocketMessage<UserVoiceOnOffData>) => {
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, true);
};
export const handleCallUserVoiceOff = (msg: WebSocketMessage) => {
export const handleCallUserVoiceOff = (msg: WebSocketMessage<UserVoiceOnOffData>) => {
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, false);
};
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage<CallStartData>) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
@ -66,7 +75,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => {
});
};
export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage<EmptyData>) => {
DeviceEventEmitter.emit(WebsocketEvents.CALLS_CALL_END, {
channelId: msg.broadcast.channel_id,
});
@ -74,38 +83,38 @@ export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage) => {
callEnded(serverUrl, msg.broadcast.channel_id);
};
export const handleCallChannelEnabled = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallChannelEnabled = (serverUrl: string, msg: WebSocketMessage<EmptyData>) => {
setChannelEnabled(serverUrl, msg.broadcast.channel_id, true);
};
export const handleCallChannelDisabled = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallChannelDisabled = (serverUrl: string, msg: WebSocketMessage<EmptyData>) => {
setChannelEnabled(serverUrl, msg.broadcast.channel_id, false);
};
export const handleCallScreenOn = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallScreenOn = (serverUrl: string, msg: WebSocketMessage<UserScreenOnOffData>) => {
setCallScreenOn(serverUrl, msg.broadcast.channel_id, msg.data.userID);
};
export const handleCallScreenOff = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallScreenOff = (serverUrl: string, msg: WebSocketMessage<UserScreenOnOffData>) => {
setCallScreenOff(serverUrl, msg.broadcast.channel_id);
};
export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage<UserRaiseUnraiseHandData>) => {
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand);
};
export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage<UserRaiseUnraiseHandData>) => {
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand);
};
export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage<UserReactionData>) => {
userReacted(serverUrl, msg.broadcast.channel_id, msg.data);
};
export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessage<CallRecordingStateData>) => {
setRecordingState(serverUrl, msg.broadcast.channel_id, msg.data.recState);
};
export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage) => {
export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage<CallHostChangedData>) => {
setHost(serverUrl, msg.broadcast.channel_id, msg.data.hostID);
};

View file

@ -1,210 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {EventEmitter} from 'events';
import {
MediaStream,
MediaStreamTrack,
RTCIceCandidate,
RTCPeerConnection,
RTCPeerConnectionIceEvent,
RTCRtpSender,
RTCSessionDescription,
} from 'react-native-webrtc';
import {logDebug, logError} from '@utils/log';
import type {RTCPeerConfig} from './types';
import type RTCTrackEvent from 'react-native-webrtc/lib/typescript/RTCTrackEvent';
const rtcConnFailedErr = new Error('rtc connection failed');
export default class RTCPeer extends EventEmitter {
private pc: RTCPeerConnection | null;
private readonly senders: { [key: string]: RTCRtpSender };
private candidates: RTCIceCandidate[] = [];
private makingOffer = false;
public connected: boolean;
constructor(config: RTCPeerConfig) {
super();
// We keep a map of track IDs -> RTP sender so that we can easily
// replace tracks when muting/unmuting.
this.senders = {};
this.pc = new RTCPeerConnection(config);
this.pc.onnegotiationneeded = () => this.onNegotiationNeeded();
this.pc.onicecandidate = (ev) => this.onICECandidate(ev);
this.pc.oniceconnectionstatechange = () => this.onICEConnectionStateChange();
this.pc.onconnectionstatechange = () => this.onConnectionStateChange();
this.pc.ontrack = (ev) => this.onTrack(ev);
this.connected = false;
// We create a data channel for two reasons:
// - Initiate a connection without preemptively adding audio/video tracks.
// - Use this communication channel for further negotiation (to be implemented).
this.pc.createDataChannel('calls-dc');
}
private onICECandidate(ev: RTCPeerConnectionIceEvent) {
if (ev.candidate) {
this.emit('candidate', ev.candidate);
}
}
private onConnectionStateChange() {
switch (this.pc?.connectionState) {
case 'connected':
this.connected = true;
break;
case 'failed':
this.emit('close', rtcConnFailedErr);
break;
}
}
private onICEConnectionStateChange() {
switch (this.pc?.iceConnectionState) {
case 'connected':
this.emit('connect');
break;
case 'failed':
this.emit('close', rtcConnFailedErr);
break;
case 'closed':
this.emit('close');
break;
default:
}
}
private async onNegotiationNeeded() {
try {
this.makingOffer = true;
await this.pc?.setLocalDescription();
this.emit('offer', this.pc?.localDescription);
} catch (err) {
this.emit('error', err);
} finally {
this.makingOffer = false;
}
}
private onTrack(ev: RTCTrackEvent) {
if (ev.streams.length === 0) {
this.emit('stream', new MediaStream([ev.track]));
return;
}
this.emit('stream', ev.streams[0]);
}
public async signal(data: string) {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
const msg = JSON.parse(data);
if (msg.type === 'offer' && (this.makingOffer || this.pc?.signalingState !== 'stable')) {
logDebug('signaling conflict, we are polite, proceeding...');
}
try {
switch (msg.type) {
case 'candidate':
// It's possible that ICE candidates are received moments before
// we set the initial remote description which would cause an
// error. In such case we queue them up to be added later.
if (this.pc.remoteDescription && this.pc.remoteDescription.type) {
this.pc.addIceCandidate(msg.candidate).catch((err) => {
logError('failed to add candidate', err);
});
} else {
logDebug('received ice candidate before remote description, queuing...');
this.candidates.push(msg.candidate);
}
break;
case 'offer':
await this.pc.setRemoteDescription(new RTCSessionDescription(msg));
await this.pc.setLocalDescription();
this.emit('answer', this.pc.localDescription);
break;
case 'answer':
await this.pc.setRemoteDescription(msg);
for (const candidate of this.candidates) {
logDebug('adding queued ice candidate');
this.pc.addIceCandidate(candidate).catch((err) => {
logError('failed to add candidate', err);
});
}
break;
default:
this.emit('error', Error('invalid signaling data received'));
}
} catch (err) {
this.emit('error', err);
}
}
public async addTrack(track: MediaStreamTrack, stream?: MediaStream) {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
const sender = await this.pc.addTrack(track, stream!);
if (sender) {
this.senders[track.id] = sender;
}
}
public addStream(stream: MediaStream) {
stream.getTracks().forEach((track) => {
this.addTrack(track, stream);
});
}
public replaceTrack(oldTrackID: string, newTrack: MediaStreamTrack | null) {
const sender = this.senders[oldTrackID];
if (!sender) {
throw new Error('sender for track not found');
}
if (newTrack && newTrack.id !== oldTrackID) {
delete this.senders[oldTrackID];
this.senders[newTrack.id] = sender;
}
sender.replaceTrack(newTrack);
}
public getStats() {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
return this.pc.getStats();
}
public destroy() {
if (!this.pc) {
throw new Error('peer has been destroyed already');
}
this.removeAllListeners('candidate');
this.removeAllListeners('connect');
this.removeAllListeners('error');
this.removeAllListeners('close');
this.removeAllListeners('offer');
this.removeAllListeners('answer');
this.removeAllListeners('stream');
this.pc.onnegotiationneeded = null;
this.pc.onicecandidate = null;
this.pc.oniceconnectionstatechange = null;
this.pc.onconnectionstatechange = null;
this.pc.ontrack = null;
this.pc.close();
this.pc = null;
this.connected = false;
}
}

View file

@ -1,8 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {RTCIceServer} from 'react-native-webrtc';
export type RTCPeerConfig = {
iceServers: RTCIceServer[];
}

View file

@ -6,6 +6,7 @@ import {useIntl} from 'react-intl';
import {
DeviceEventEmitter,
Keyboard,
NativeModules,
Platform,
Pressable,
SafeAreaView,
@ -42,6 +43,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import WebsocketManager from '@managers/websocket_manager';
import {
allOrientations,
bottomSheet,
dismissAllModalsAndPopToScreen,
dismissBottomSheet,
@ -50,6 +52,7 @@ import {
setScreensOrientation,
} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {freezeOtherScreens} from '@utils/gallery';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {mergeNavigationOptions} from '@utils/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -107,8 +110,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
position: 'absolute',
top: 0,
backgroundColor: 'rgba(0,0,0,0.64)',
height: 64,
padding: 0,
height: 52,
paddingTop: 0,
},
headerLandscapeNoControls: {
top: -1000,
@ -160,11 +163,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
}),
},
buttonsLandscape: {
height: 128,
height: 110,
position: 'absolute',
backgroundColor: 'rgba(0,0,0,0.64)',
bottom: 0,
},
buttonsLandscapeWithReactions: {
height: 174,
},
buttonsLandscapeNoControls: {
bottom: 1000,
},
@ -173,6 +179,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
alignItems: 'center',
flex: 1,
},
buttonLandscape: {
flex: 0,
},
mute: {
flexDirection: 'column',
alignItems: 'center',
@ -198,7 +207,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
otherButtons: {
flexDirection: 'row',
alignItems: 'center',
alignContent: 'space-between',
},
otherButtonsLandscape: {
justifyContent: 'center',
},
collapseIcon: {
color: theme.sidebarText,
@ -208,6 +219,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
borderRadius: 4,
overflow: 'hidden',
},
collapseIconLandscape: {
margin: 10,
padding: 0,
backgroundColor: 'transparent',
borderRadius: 0,
},
muteIcon: {
color: theme.sidebarText,
},
@ -230,6 +247,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
margin: 10,
overflow: 'hidden',
},
buttonIconLandscape: {
borderRadius: 26,
paddingTop: 14,
paddingRight: 16,
paddingBottom: 16,
paddingLeft: 14,
width: 52,
height: 52,
marginLeft: 12,
marginRight: 12,
},
hangUpIcon: {
backgroundColor: Preferences.THEMES.denim.dndIndicator,
},
@ -272,7 +300,6 @@ const CallScreen = ({
const style = getStyleSheet(theme);
const isLandscape = width > height;
const showControls = !isLandscape || showControlsInLandscape;
const myParticipant = currentCall?.participants[currentCall.myUserId];
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
@ -285,11 +312,24 @@ const CallScreen = ({
mergeNavigationOptions('Call', {
layout: {
componentBackgroundColor: 'black',
orientation: allOrientations,
},
topBar: {
visible: false,
},
});
if (Platform.OS === 'ios') {
NativeModules.SplitView.unlockOrientation();
}
return () => {
setScreensOrientation(isTablet);
if (Platform.OS === 'ios' && !isTablet) {
// We need both the navigation & the module
NativeModules.SplitView.lockPortrait();
}
freezeOtherScreens(false);
};
}, []);
const leaveCallHandler = useCallback(() => {
@ -473,12 +513,14 @@ const CallScreen = ({
streamURL={currentCall.screenShareURL}
style={style.screenShareImage}
/>
<FormattedText
id={'mobile.calls_viewing_screen'}
defaultMessage={'You are viewing {name}\'s screen'}
values={{name: displayUsername(participantsDict[currentCall.screenOn].userModel, intl.locale, teammateNameDisplay)}}
style={style.screenShareText}
/>
{!isLandscape &&
<FormattedText
id={'mobile.calls_viewing_screen'}
defaultMessage={'You are viewing {name}\'s screen'}
values={{name: displayUsername(participantsDict[currentCall.screenOn].userModel, intl.locale, teammateNameDisplay)}}
style={style.screenShareText}
/>
}
</Pressable>
);
}
@ -545,7 +587,11 @@ const CallScreen = ({
<SafeAreaView style={style.wrapper}>
<View style={style.container}>
<View
style={[style.header, isLandscape && style.headerLandscape, !showControls && style.headerLandscapeNoControls]}
style={[
style.header,
isLandscape && style.headerLandscape,
isLandscape && !showControlsInLandscape && style.headerLandscapeNoControls,
]}
>
{waitingForRecording && <CallsBadge type={CallsBadgeType.Waiting}/>}
{recording && <CallsBadge type={CallsBadgeType.Rec}/>}
@ -558,18 +604,27 @@ const CallScreen = ({
<CompassIcon
name='arrow-collapse'
size={24}
style={style.collapseIcon}
style={[style.collapseIcon, isLandscape && style.collapseIconLandscape]}
/>
</Pressable>
</View>
{usersList}
{screenShareView}
{micPermissionsError && <PermissionErrorBar/>}
<EmojiList reactionStream={currentCall.reactionStream}/>
{showReactions && <ReactionBar raisedHand={myParticipant.raisedHand}/>}
{!isLandscape &&
<EmojiList reactionStream={currentCall.reactionStream}/>
}
<View
style={[style.buttons, isLandscape && style.buttonsLandscape, !showControls && style.buttonsLandscapeNoControls]}
style={[
style.buttons,
isLandscape && style.buttonsLandscape,
isLandscape && showReactions && style.buttonsLandscapeWithReactions,
isLandscape && !showControlsInLandscape && style.buttonsLandscapeNoControls,
]}
>
{showReactions &&
<ReactionBar raisedHand={myParticipant.raisedHand}/>
}
{!isLandscape &&
<Pressable
testID='mute-unmute'
@ -584,17 +639,18 @@ const CallScreen = ({
style={style.muteIcon}
/>
{myParticipant.muted ? UnmuteText : MuteText}
</Pressable>}
<View style={style.otherButtons}>
</Pressable>
}
<View style={[style.otherButtons, isLandscape && style.otherButtonsLandscape]}>
<Pressable
testID='leave'
style={style.button}
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={leaveCallHandler}
>
<CompassIcon
name='phone-hangup'
size={24}
style={{...style.buttonIcon, ...style.hangUpIcon}}
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, style.hangUpIcon]}
/>
<FormattedText
id={'mobile.calls_leave'}
@ -604,13 +660,18 @@ const CallScreen = ({
</Pressable>
<Pressable
testID={'toggle-speakerphone'}
style={style.button}
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={toggleSpeakerPhone}
>
<CompassIcon
name={'volume-high'}
size={24}
style={[style.buttonIcon, style.speakerphoneIcon, currentCall.speakerphoneOn && style.buttonOn]}
style={[
style.buttonIcon,
isLandscape && style.buttonIconLandscape,
style.speakerphoneIcon,
currentCall.speakerphoneOn && style.buttonOn,
]}
/>
<FormattedText
id={'mobile.calls_speaker'}
@ -619,13 +680,13 @@ const CallScreen = ({
/>
</Pressable>
<Pressable
style={style.button}
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={toggleReactions}
>
<CompassIcon
name={'emoticon-happy-outline'}
size={24}
style={[style.buttonIcon, showReactions && style.buttonOn]}
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape, showReactions && style.buttonOn]}
/>
<FormattedText
id={'mobile.calls_react'}
@ -634,13 +695,13 @@ const CallScreen = ({
/>
</Pressable>
<Pressable
style={style.button}
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={showOtherActions}
>
<CompassIcon
name='dots-horizontal'
size={24}
style={style.buttonIcon}
style={[style.buttonIcon, isLandscape && style.buttonIconLandscape]}
/>
<FormattedText
id={'mobile.calls_more'}
@ -651,16 +712,22 @@ const CallScreen = ({
{isLandscape &&
<Pressable
testID='mute-unmute'
style={style.button}
style={[style.button, style.buttonLandscape]}
onPress={muteUnmuteHandler}
>
<CompassIcon
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
size={24}
style={[style.buttonIcon, style.muteIconLandscape, myParticipant?.muted && style.muteIconLandscapeMuted]}
style={[
style.buttonIcon,
isLandscape && style.buttonIconLandscape,
style.muteIconLandscape,
myParticipant?.muted && style.muteIconLandscapeMuted,
]}
/>
{myParticipant.muted ? UnmuteText : MuteText}
</Pressable>}
</Pressable>
}
</View>
</View>
</View>

View file

@ -40,8 +40,6 @@ import {
setPluginEnabled,
setUserVoiceOn,
} from '@calls/state/actions';
import {License} from '@constants';
import {
Call,
CallsState,
@ -51,8 +49,10 @@ import {
DefaultCurrentCall,
DefaultGlobalCallsState,
GlobalCallsState,
RecordingState,
} from '../types/calls';
} from '@calls/types/calls';
import {License} from '@constants';
import type {CallRecordingState} from '@mattermost/calls/lib/types';
jest.mock('@calls/alerts');
@ -312,6 +312,66 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], expectedCurrentCallState);
});
it('leftCall with screensharing on', () => {
const initialCallsState: CallsState = {
...DefaultCallsState,
calls: {
'channel-1': {
...call1,
screenOn: 'user-1',
},
},
};
const initialChannelsWithCallsState = {
'channel-1': true,
};
const initialCurrentCallState: CurrentCall = {
...DefaultCurrentCall,
connected: true,
serverUrl: 'server1',
myUserId: 'myUserId',
...call1,
screenOn: 'user-1',
};
const expectedCallsState = {
'channel-1': {
participants: {
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
},
channelId: 'channel-1',
startTime: 123,
threadId: 'thread-1',
ownerId: 'user-1',
hostId: 'user-1',
screenOn: '',
},
};
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
const expectedCurrentCallState: CurrentCall = {
...initialCurrentCallState,
...expectedCallsState['channel-1'],
};
// setup
const {result} = renderHook(() => {
return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()];
});
act(() => {
setCallsState('server1', initialCallsState);
setChannelsWithCalls('server1', initialChannelsWithCallsState);
setCurrentCall(initialCurrentCallState);
});
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], initialChannelsWithCallsState);
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => userLeftCall('server1', 'channel-1', 'user-1'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
});
it('callStarted', () => {
// setup
const {result} = renderHook(() => {
@ -797,6 +857,7 @@ describe('useCallsState', () => {
it('config', () => {
const newConfig = {
...DefaultCallsConfig,
ICEServers: [],
ICEServersConfigs: [
{
@ -914,7 +975,7 @@ describe('useCallsState', () => {
myUserId: 'myUserId',
...call1,
};
const recState: RecordingState = {
const recState: CallRecordingState = {
init_at: 123,
start_at: 231,
end_at: 345,

View file

@ -16,17 +16,17 @@ import {
} from '@calls/state';
import {
Call,
CallReaction,
CallsConfig,
CallsConfigState,
ChannelsWithCalls,
CurrentCall,
DefaultCall,
DefaultCurrentCall,
ReactionStreamEmoji,
RecordingState,
} from '@calls/types/calls';
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
import type {CallRecordingState, UserReactionData} from '@mattermost/calls/lib/types';
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
const channelsWithCalls = Object.keys(calls).reduce(
(accum, next) => {
@ -141,6 +141,12 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
participants: {...callsState.calls[channelId].participants},
};
delete nextCall.participants[userId];
// If they were screensharing, remove that.
if (nextCall.screenOn === userId) {
nextCall.screenOn = '';
}
const nextCalls = {...callsState.calls};
if (Object.keys(nextCall.participants).length === 0) {
delete nextCalls[channelId];
@ -177,6 +183,12 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
voiceOn,
};
delete nextCurrentCall.participants[userId];
// If they were screensharing, remove that.
if (nextCurrentCall.screenOn === userId) {
nextCurrentCall.screenOn = '';
}
setCurrentCall(nextCurrentCall);
};
@ -390,7 +402,7 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => {
}
};
export const setConfig = (serverUrl: string, config: Partial<CallsConfig>) => {
export const setConfig = (serverUrl: string, config: Partial<CallsConfigState>) => {
const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, ...config});
};
@ -423,7 +435,7 @@ export const setMicPermissionsErrorDismissed = () => {
setCurrentCall(nextCurrentCall);
};
export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => {
export const userReacted = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
// Note: Simplification for performance:
// If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your
// servers, do we want to be tracking reactions and setting timeouts for all those calls? No.
@ -475,7 +487,7 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: Call
}, REACTION_TIMEOUT);
};
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => {
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
const currentCall = getCurrentCall();
if (currentCall?.channelId !== channelId) {
return;
@ -499,7 +511,7 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Cal
setCurrentCall(nextCurrentCall);
};
export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => {
export const setRecordingState = (serverUrl: string, channelId: string, recState: CallRecordingState) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;

View file

@ -4,9 +4,9 @@
import {useEffect, useState} from 'react';
import {BehaviorSubject} from 'rxjs';
import {CallsConfig, DefaultCallsConfig} from '@calls/types/calls';
import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfig>> = {};
const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfigState>> = {};
const getCallsConfigSubject = (serverUrl: string) => {
if (!callsConfigSubjects[serverUrl]) {
@ -20,7 +20,7 @@ export const getCallsConfig = (serverUrl: string) => {
return getCallsConfigSubject(serverUrl).value;
};
export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfig) => {
export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfigState) => {
getCallsConfigSubject(serverUrl).next(callsConfig);
};

View file

@ -1,8 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {CallRecordingState, CallsConfig, EmojiData, UserReactionData} from '@mattermost/calls/lib/types';
import type UserModel from '@typings/database/models/servers/user';
import type {RTCIceServer} from 'react-native-webrtc';
export type GlobalCallsState = {
micPermissionsGranted: boolean;
@ -31,7 +31,7 @@ export type Call = {
screenOn: string;
threadId: string;
ownerId: string;
recState?: RecordingState;
recState?: CallRecordingState;
hostId: string;
}
@ -75,7 +75,7 @@ export type CallParticipant = {
muted: boolean;
raisedHand: number;
userModel?: UserModel;
reaction?: CallReaction;
reaction?: UserReactionData;
}
export type ChannelsWithCalls = Dictionary<boolean>;
@ -100,7 +100,7 @@ export type ServerCallState = {
screen_sharing_id: string;
owner_id: string;
host_id: string;
recording: RecordingState;
recording: CallRecordingState;
}
export type CallsConnection = {
@ -111,26 +111,16 @@ export type CallsConnection = {
raiseHand: () => void;
unraiseHand: () => void;
initializeVoiceTrack: () => void;
sendReaction: (emoji: CallReactionEmoji) => void;
sendReaction: (emoji: EmojiData) => void;
}
export type ServerCallsConfig = {
ICEServers?: string[]; // deprecated
ICEServersConfigs: RTCIceServer[];
export type CallsConfigState = CallsConfig & {
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
NeedsTURNCredentials: boolean;
sku_short_name: string;
MaxCallParticipants: number;
EnableRecordings: boolean;
}
export type CallsConfig = ServerCallsConfig & {
pluginEnabled: boolean;
last_retrieved_at: number;
}
export const DefaultCallsConfig: CallsConfig = {
export const DefaultCallsConfig: CallsConfigState = {
pluginEnabled: false,
ICEServers: [], // deprecated
ICEServersConfigs: [],
@ -141,6 +131,8 @@ export const DefaultCallsConfig: CallsConfig = {
sku_short_name: '',
MaxCallParticipants: 0,
EnableRecordings: false,
MaxRecordingDuration: 60,
AllowScreenSharing: true,
};
export type ApiResp = {
@ -149,29 +141,9 @@ export type ApiResp = {
status_code: number;
}
export type CallReactionEmoji = {
name: string;
skin?: string;
unified: string;
literal?: string;
}
export type CallReaction = {
user_id: string;
emoji: CallReactionEmoji;
timestamp: number;
}
export type ReactionStreamEmoji = {
name: string;
latestTimestamp: number;
count: number;
literal?: string;
};
export type RecordingState = {
init_at: number;
start_at: number;
end_at: number;
err?: string;
}

View file

@ -3,15 +3,15 @@
import assert from 'assert';
import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
import {License} from '@constants';
import {getICEServersConfigs} from './utils';
import type {CallsConfig} from '@calls/types/calls';
describe('getICEServersConfigs', () => {
it('backwards compatible case, no ICEServersConfigs present', () => {
const config: CallsConfig = {
const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true,
ICEServers: ['stun:stun.example.com:3478'],
ICEServersConfigs: [],
@ -33,7 +33,8 @@ describe('getICEServersConfigs', () => {
});
it('ICEServersConfigs set', () => {
const config: CallsConfig = {
const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true,
ICEServersConfigs: [
{
@ -64,7 +65,8 @@ describe('getICEServersConfigs', () => {
});
it('Both ICEServers and ICEServersConfigs set', () => {
const config: CallsConfig = {
const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true,
ICEServers: ['stun:stuna.example.com:3478'],
ICEServersConfigs: [

View file

@ -8,9 +8,11 @@ import Calls from '@constants/calls';
import {isMinimumServerVersion} from '@utils/helpers';
import {displayUsername} from '@utils/user';
import type {CallParticipant, ServerCallsConfig} from '@calls/types/calls';
import type {CallParticipant} from '@calls/types/calls';
import type {CallsConfig} from '@mattermost/calls/lib/types';
import type PostModel from '@typings/database/models/servers/post';
import type {IntlShape} from 'react-intl';
import type {RTCIceServer} from 'react-native-webrtc';
export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
if (!participants) {
@ -38,12 +40,6 @@ const sortByState = (presenterID?: string) => {
return 1;
}
if (!a.muted && b.muted) {
return -1;
} else if (!b.muted && a.muted) {
return 1;
}
if (a.raisedHand && !b.raisedHand) {
return -1;
} else if (b.raisedHand && !a.raisedHand) {
@ -52,6 +48,12 @@ const sortByState = (presenterID?: string) => {
return a.raisedHand - b.raisedHand;
}
if (!a.muted && b.muted) {
return -1;
} else if (!b.muted && a.muted) {
return 1;
}
return 0;
};
};
@ -106,7 +108,7 @@ export function errorAlert(error: string, intl: IntlShape) {
);
}
export function getICEServersConfigs(config: ServerCallsConfig) {
export function getICEServersConfigs(config: CallsConfig): RTCIceServer[] {
// if ICEServersConfigs is set, we can trust this to be complete and
// coming from an updated API.
if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) {

View file

@ -12,6 +12,7 @@ import {subscribeAllServers} from '@database/subscription/servers';
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
import {useAppState} from '@hooks/device';
import NativeNotification from '@notifications';
import {logDebug} from '@utils/log';
import {changeOpacity} from '@utils/theme';
import type ServersModel from '@typings/database/models/app/servers';
@ -56,9 +57,11 @@ const Home = ({isFocused, theme}: Props) => {
if (Platform.OS === 'ios') {
NativeNotification.getDeliveredNotifications().then((delivered) => {
if (mentions === 0 && delivered.length > 0) {
logDebug('Not updating badge count, since we have no mentions in the database, and the number of notifications in the notification center is', delivered.length);
return;
}
logDebug('Setting the badge count based on database values to', mentions);
Notifications.ios.setBadgeCount(mentions);
});
}

View file

@ -24,7 +24,7 @@ export function makeCategoryChannelId(teamId: string, channelId: string) {
export const isUnreadChannel = (myChannel: MyChannelModel, notifyProps?: Partial<ChannelNotifyProps>, lastUnreadChannelId?: string) => {
const isMuted = notifyProps?.mark_unread === General.MENTION;
return (isMuted && myChannel.mentionsCount) || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId);
return myChannel.mentionsCount || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId);
};
export const filterArchivedChannels = (channelsWithMyChannel: ChannelWithMyChannel[], currentChannelId: string) => {

View file

@ -16,7 +16,7 @@ import type {IntlShape} from 'react-intl';
const MattermostManaged = NativeModules.MattermostManaged;
type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo';
type PermissionSource = 'camera' | 'storage' | 'photo_android' | 'photo_ios' | 'photo';
export default class FilePickerUtil {
private readonly uploadFiles: (files: ExtractedFileInfo[]) => void;
@ -64,7 +64,7 @@ export default class FilePickerUtil {
'Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.',
}, {applicationName}),
},
denied_ios: {
photo_ios: {
title: formatMessage(
{
id: 'mobile.ios.photos_permission_denied_title',
@ -79,7 +79,7 @@ export default class FilePickerUtil {
'Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.',
}, {applicationName}),
},
denied_android: {
photo_android: {
title: formatMessage(
{
id: 'mobile.android.photos_permission_denied_title',
@ -109,8 +109,8 @@ export default class FilePickerUtil {
};
private getPermissionDeniedMessage = (source?: PermissionSource) => {
const sources = ['camera', 'storage', 'photo'];
const deniedSource: PermissionSource = Platform.select({android: 'denied_android', ios: 'denied_ios'})!;
const sources = ['camera', 'storage'];
const deniedSource: PermissionSource = Platform.select({android: 'photo_android', ios: 'photo_ios'})!;
const msgForSource = source && sources.includes(source) ? source : deniedSource;
return this.getPermissionMessages(msgForSource);

View file

@ -992,5 +992,10 @@
"invite.members.user_is_guest": "",
"invite.members.already_member": "",
"mobile.calls_start_call_exists": "",
"mobile.calls_not_connected": ""
"mobile.calls_not_connected": "",
"channel_notification_preferences.notification.thread_replies": "M'avertir des réponses aux fils de discussion que je suis sur cette chaîne",
"channel_notification_preferences.notification.mention": "Mentions, messages directs uniquement",
"channel_notification_preferences.notification.all": "Tous les nouveaux messages",
"channel_notification_preferences.muted_title": "Ce canal est en sourdine",
"channel_notification_preferences.muted_content": "Vous pouvez modifier les paramètres de notification, mais vous ne recevrez pas de notifications tant que le canal n'aura pas été désactivé."
}

View file

@ -398,7 +398,7 @@
"channel_info.copied": "Kopyalandı",
"channel_info.convert_private_title": "{displayName} kanalı özel kanala dönüştürülsün mü?",
"channel_info.convert_private_success": "{displayName} özel bir kanala dönüştürüldü.",
"channel_info.convert_private_description": "{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?",
"channel_info.convert_private_description": "{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şmeyi sürdürebilir. Ö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?",
"channel_info.convert_private": "Özel kanala dönüştür",
"channel_info.convert_failed": "{displayName} özel kanala dönüştürülemedi.",
"channel_info.close_dm_channel": "Bu doğrudan iletiyi kapatmak istediğinize emin misiniz? Bu işlem iletiyi giriş sayfanızdan kaldıracak, ancak istediğiniz zaman yeniden açabilirsiniz.",
@ -549,7 +549,7 @@
"mobile.create_direct_message.start": "Görüşmeyi başlat",
"mobile.create_channel.title": "Yeni kanal",
"mobile.components.select_server_view.msg_welcome": "Hoş geldiniz",
"mobile.components.select_server_view.msg_description": "Bir sunucuya benzersiz bir adres üzerinden erişilir ve takımınızın iletişim merkezidir",
"mobile.components.select_server_view.msg_description": "Bir sunucu takımınızın benzersiz bir adres üzerinden erişilen iletişim merkezidir",
"mobile.components.select_server_view.msg_connect": "Bir sunucu ile bağlantı kuralım",
"mobile.components.select_server_view.displayName": "Görüntülenecek ad",
"mobile.components.select_server_view.displayHelp": "Sunucunuzun görüntülenecek adını seçin",
@ -971,7 +971,7 @@
"display_settings.crt.off": "Kapat",
"display_settings.crt": "Daraltılmış yanıt konuları",
"channel_info.archive_description.cannot_view_archived": "Bu işlem kanalı takımdan arşivleyecek ve içeriğine hiç bir kullanıcı erişemeyecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?",
"channel_info.archive_description.can_view_archived": "Bu işlem takım kanalını arşivleyecek. Kanal üyeleri kanal içeriğini görmeye devam edebilecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?",
"channel_info.archive_description.can_view_archived": "Bu işlem takım kanalını arşivleyecek. Kanal üyeleri kanal içeriğini görmeyi sürdürebilecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?",
"snack.bar.remove.user": "1 üye kanaldan çıkarıldı",
"mobile.manage_members.section_title_members": "ÜYELER",
"mobile.manage_members.section_title_admins": "KANAL YÖNETİCİLERİ",
@ -987,10 +987,24 @@
"mobile.manage_members.change_role.error": "Rol güncellenirken bir sorun çıktı. Lütfen bağlantınızı denetleyip yeniden deneyin.",
"mobile.manage_members.cancel": "İptal",
"mobile.manage_members.admin": "Yönetici",
"mobile.calls_recording_stop_none_in_progress": "",
"mobile.calls_recording_stop_no_permissions": "",
"mobile.calls_recording_start_no_permissions": "",
"mobile.calls_recording_start_in_progress": "",
"mobile.calls_host_rec_error_title": "",
"mobile.calls_host_rec_error": ""
"mobile.calls_recording_stop_none_in_progress": "Sürmekte olan bir kayıt yok.",
"mobile.calls_recording_stop_no_permissions": "Kaydı durdurma izniniz yok. Lütfen çağrı sahibinden kaydı durdurmasını isteyin.",
"mobile.calls_recording_start_no_permissions": "Kayıt başlatma izniniz yok. Lütfen çağrı sahibinden kaydı başlatmasını isteyin.",
"mobile.calls_recording_start_in_progress": "Sürmekte olan bir kayıt var.",
"mobile.calls_host_rec_error_title": "Kayıtta bir sorun çıktı",
"mobile.calls_host_rec_error": "Lütfen yeniden kaydetmeyi deneyin. Sorun giderme yardımı için sistem yöneticinizle de görüşebilirsiniz.",
"notification.no_post": "İleti bulunamadı.",
"notification.no_connection": "Sunucuya ulaşılamadığından bildirim kanalını / takımını alamadık.",
"invite.summary.back": "Geri dön",
"channel_notification_preferences.muted_title": "Bu kanalın bildirimleri kapatılmış",
"channel_notification_preferences.muted_content": "Bildirim ayarlarını değiştirebilirsiniz, ancak kanal bildirimleri açılana kadar bildirim almazsınız.",
"channel_notification_preferences.unmute_content": "Kanal bildirimlerini aç",
"channel_notification_preferences.thread_replies": "Konu yanıtları",
"channel_notification_preferences.reset_default": "Varsayılana dön",
"channel_notification_preferences.notify_about": "Bana şunlar bildirilsin...",
"channel_notification_preferences.notification.thread_replies": "Bu kanalda izlediğim konulara verilen yanıtlar bana bildirilsin",
"channel_notification_preferences.notification.none": "Hiçbir şey",
"channel_notification_preferences.notification.mention": "Anmalar, yalnızca doğrudan iletiler",
"channel_notification_preferences.notification.all": "Tüm yeni iletiler",
"channel_notification_preferences.default": "(varsayılan)"
}

View file

@ -5,7 +5,7 @@
"about.enterpriseEditionSt": "位于防火墙后的现代通讯方式。",
"about.hash": "编译哈希:",
"about.hashee": "企业版编译哈希:",
"about.teamEditionLearn": "加入 Mattermost 社区: ",
"about.teamEditionLearn": "加入 Mattermost 社区:",
"about.teamEditionSt": "所有团队的通讯一站式解决,随时随地可搜索和访问。",
"about.teamEditiont0": "团队版",
"about.teamEditiont1": "企业版",
@ -391,28 +391,28 @@
"account.logout": "注销",
"user_status.dnd": "",
"video.failed_description": "",
"invite_people_to_team.title": "",
"mobile.calls_unmute": "",
"invite_people_to_team.title": "参加{team} 团队",
"mobile.calls_unmute": "解除静音",
"server.remove.alert_description": "",
"mobile.managed.jailbreak_no_debug_info": "",
"channel_info.notification.mention": "",
"mobile.managed.jailbreak_no_debug_info": "无效",
"channel_info.notification.mention": "提及",
"notification_settings.pushNotification.mentions_only": "",
"channel_info.close_dm_channel": "",
"channel_info.close_dm_channel": "您确定关闭此对话吗?会话将从主屏幕中删除,但您可以再次打开它。",
"notification_settings.email.crt.send": "",
"snack.bar.unfavorite.channel": "",
"post_info.guest": "",
"notification_settings.send_notification.about": "",
"settings.display": "",
"mobile.managed.jailbreak_no_reason": "",
"mobile.managed.jailbreak_no_reason": "无效",
"mobile.write_storage_permission_denied_description": "",
"post_info.auto_responder": "",
"default_skin_tone": "",
"default_skin_tone": "默认皮肤颜色",
"invite_people_to_team.message": "",
"mobile.add_team.create_team": "",
"mobile.add_team.create_team": "创建一个新团队",
"user_status.offline": "",
"servers.login": "",
"search_bar.search.placeholder": "",
"extension.no_servers.title": "",
"extension.no_servers.title": "没有连接到任何服务器",
"screen.search.results.filter.images": "",
"mobile.server_upgrade.description": "",
"server.logout.alert_description": "",
@ -420,27 +420,27 @@
"mobile.manage_members.section_title_admins": "",
"mobile.manage_members.remove_member": "",
"mobile.manage_members.remove": "",
"mobile.manage_members.message": "",
"mobile.manage_members.member": "",
"mobile.manage_members.manage_member": "",
"mobile.manage_members.make_channel_member": "",
"mobile.manage_members.message": "您确定从此频道中删除选中的成员吗?",
"mobile.manage_members.member": "成员",
"mobile.manage_members.manage_member": "管理成员",
"mobile.manage_members.make_channel_member": "成为频道成员",
"mobile.display_settings.crt": "",
"mobile.calls_recording_stop_no_permissions": "",
"mobile.calls_recording_start_no_permissions": "",
"mobile.calls_recording_start_in_progress": "",
"mobile.calls_react": "",
"mobile.calls_participant_rec_title": "",
"mobile.calls_okay": "",
"mobile.calls_lower_hand": "",
"mobile.calls_host_rec_error_title": "",
"mobile.calls_host_rec_error": "",
"mobile.calls_recording_stop_no_permissions": "您没有停止录制的权限。请联系通话的主持人停止录制。",
"mobile.calls_recording_start_no_permissions": "您没有开始录制的权限。请联系通话的主持人开始录制。",
"mobile.calls_recording_start_in_progress": "录制已经在进行中了。",
"mobile.calls_react": "反馈",
"mobile.calls_participant_rec_title": "录制正在进行中",
"mobile.calls_okay": "好的",
"mobile.calls_lower_hand": "放下手",
"mobile.calls_host_rec_error_title": "此录制有错误发生",
"mobile.calls_host_rec_error": "请尝试再次录制。您也可以联系系统管理员寻求帮助。",
"thread.header.thread_in": "",
"snack.bar.remove.user": "",
"mobile.manage_members.section_title_members": "",
"mobile.manage_members.make_channel_admin": "",
"mobile.manage_members.done": "",
"mobile.manage_members.cancel": "",
"mobile.manage_members.admin": "",
"mobile.manage_members.make_channel_admin": "成为频道管理员",
"mobile.manage_members.done": "完成",
"mobile.manage_members.cancel": "取消",
"mobile.manage_members.admin": "管理者",
"team_list.no_other_teams.title": "",
"team_list.no_other_teams.description": "",
"share_extension.max_resolution": "",
@ -449,55 +449,55 @@
"settings.advanced.delete_data": "",
"settings_display.crt.label": "",
"settings_display.crt.desc": "",
"mobile.calls_request_title": "",
"mobile.calls_request_message": "",
"channel_info.archive_description.cannot_view_archived": "",
"mobile.calls_request_title": "通话或能当前没有被开启",
"mobile.calls_request_message": "通话当前正在运行在测试模式下,只有系统管理可以开始录制。请直接联系系统管理员寻求帮助",
"channel_info.archive_description.cannot_view_archived": "这将从团队中归档此频道。归档频道可以根据需要被再次恢复。\n\n您确定要归档{term} {name} 吗?",
"channel_info.archive_description.can_view_archived": "这将从团队中归档此频道。频道内容仍可由频道成员查看。\n\n您确定要归档{term} {name} 吗?",
"mobile.session_expired": "",
"mobile.calls_not_available_title": "",
"invite.title.summary": "",
"invite.title": "",
"invite.summary.email_invite": "",
"gallery.opening": "",
"edit_server.saving": "",
"display_settings.crt.on": "",
"display_settings.crt.off": "",
"display_settings.crt": "",
"mobile.calls_not_available_title": "通话没有被启用",
"invite.title.summary": "邀请概要",
"invite.title": "邀请",
"invite.summary.email_invite": "邀请邮件已发送",
"gallery.opening": "打开中…",
"edit_server.saving": "保存中",
"display_settings.crt.on": "",
"display_settings.crt.off": "",
"display_settings.crt": "收起话题回复",
"rate.subtitle": "",
"invite.summary.some_not_sent": "",
"invite.summary.smtp_failure": "",
"invite.summary.report.sent": "",
"invite.summary.report.notSent": "",
"invite.summary.not_sent": "",
"invite.summary.error": "",
"invite.summary.done": "",
"invite.shareLink": "",
"invite.sendInvitationsTo": "",
"invite.send_invite": "",
"invite.send_error": "",
"invite.searchPlaceholder": "",
"invite.search.no_results": "",
"invite.search.email_invite": "",
"invite.members.user_is_guest": "",
"invite.members.already_member": "",
"invite.summary.smtp_failure": "SMTP没有在系统控制台里配置",
"invite.summary.report.sent": "{count} 成功的 {count, plural, one {邀请} other {邀请}}",
"invite.summary.report.notSent": "{count} {count, plural, one {邀请} other {邀请}} 没有发送",
"invite.summary.not_sent": "{notSentCount, plural, one {邀请没有} other {邀请没有}} 发送",
"invite.summary.error": "{invitationsCount, plural, one {邀请} other {邀请}} 不能发送成功",
"invite.summary.done": "完成",
"invite.shareLink": "分享链接",
"invite.sendInvitationsTo": "发送邀请到…",
"invite.send_invite": "发送",
"invite.send_error": "在尝试发送邀请里发成了一些错误。请检查您的网络连接后再次尝试。",
"invite.searchPlaceholder": "输入一个名称或者邮件地址…",
"invite.search.no_results": "无人匹配",
"invite.search.email_invite": "邀请",
"invite.members.user_is_guest": "联系您的管理员将此访客变成正式的团队成员",
"invite.members.already_member": "此人已经是团队成员",
"terms_of_service.terms_declined.ok": "",
"mobile.calls_start_call_exists": "",
"mobile.calls_not_connected": "",
"mobile.calls_start_call_exists": "此频道已经有通话正在进行。",
"mobile.calls_not_connected": "您没有接入当前频道的通话。",
"skintone_selector.tooltip.title": "",
"skintone_selector.tooltip.description": "",
"video.download_description": "",
"user.edit_profile.profile_photo.change_photo": "",
"user_status.title": "",
"user_status.online": "",
"join_team.error.title": "",
"join_team.error.group_error": "",
"join_team.error.title": "加入团队在出错",
"join_team.error.group_error": "你需要是一个相关组的成员来加入团队。",
"server.logout.alert_title": "",
"screens.channel_info.gm": "",
"permalink.error.cancel": "",
"mobile.camera_type.title": "",
"mobile.calls_end_msg_dm": "",
"custom_status.set_status": "",
"connection_banner.connecting": "",
"mobile.camera_type.title": "相机选项",
"mobile.calls_end_msg_dm": "您确定结束和{displayName}的通话吗?",
"custom_status.set_status": "自定义状态",
"connection_banner.connecting": "正在连接…",
"user.settings.general.field_handled_externally": "",
"thread.loadingReplies": "",
"share_extension.multiple_label": "",
@ -509,61 +509,61 @@
"onboarding.integrations_description": "",
"onboarding.calls": "",
"notification.not_channel_member": "",
"mobile.calls_open_channel": "",
"mobile.calls_call_thread": "",
"mobile.calls_open_channel": "打开频道",
"mobile.calls_call_thread": "通话主题",
"share_extension.message": "",
"settings.advanced.delete": "",
"mobile.calls_participant_limit_title_GA": "",
"mobile.calls_participant_limit_title_GA": "此通话达到容量上限",
"mobile.calls_limit_msg_GA": "",
"general_settings.report_problem": "",
"general_settings.report_problem": "报告一个问题",
"settings.advanced.cancel": "",
"mobile.open_dm.error": "",
"mobile.create_direct_message.start": "",
"mobile.create_direct_message.max_limit_reached": "",
"mobile.create_direct_message.start": "开始会话",
"mobile.create_direct_message.max_limit_reached": "聊天最大支持 {maxCount} 个成员",
"share_extension.upload_disabled": "",
"share_extension.share_screen.title": "",
"share_extension.servers_screen.title": "",
"share_extension.server_label": "",
"notification_settings.threads_mentions": "",
"notification_settings.email.emailHelp2": "",
"mobile.custom_status.clear_after.title": "",
"mobile.custom_list.no_results": "",
"mobile.create_direct_message.you": "",
"mobile.calls_stop_recording": "",
"mobile.calls_record": "",
"mobile.calls_rec": "",
"mobile.calls_host_rec_title": "",
"mobile.calls_host_rec_stopped_title": "",
"mobile.custom_status.clear_after.title": "此时间后消除自定义状态",
"mobile.custom_list.no_results": "结果为空",
"mobile.create_direct_message.you": "@{username} - 你",
"mobile.calls_stop_recording": "停止录制",
"mobile.calls_record": "录制",
"mobile.calls_rec": "反馈",
"mobile.calls_host_rec_title": "您正在录制中",
"mobile.calls_host_rec_stopped_title": "录制已经停止。处理中…",
"mobile.calls_host_rec_stopped": "",
"mobile.calls_host_rec": "",
"mobile.calls_dismiss": "",
"mobile.announcement_banner.title": "",
"login.signingIn": "",
"login.signIn": "",
"login.invalid_credentials": "",
"connection_banner.not_reachable": "",
"connection_banner.not_connected": "",
"connection_banner.connected": "",
"mobile.calls_host_rec": "您正在录制此会议。让所有人知道这个会议正在录制可能是一个不错的主意。",
"mobile.calls_dismiss": "驳回",
"mobile.announcement_banner.title": "公告",
"login.signingIn": "登录中",
"login.signIn": "登录",
"login.invalid_credentials": "邮件和密码的组合不正确",
"connection_banner.not_reachable": "服务器无法连接",
"connection_banner.not_connected": "无互联网连接",
"connection_banner.connected": "连接已恢复",
"share_extension.file_limit.multiple": "",
"share_extension.count_limit": "",
"settings.advanced.delete_message.confirmation": "",
"notification_settings.mentions.keywords_mention": "",
"notification_settings.auto_responder.message": "",
"mobile.edit_post.delete_title": "",
"extension.no_servers.description": "",
"extension.no_memberships.title": "",
"extension.no_memberships.description": "",
"mobile.edit_post.delete_title": "确认删除",
"extension.no_servers.description": "为了分享内容你将需要先登入一个Mattermost服务器。",
"extension.no_memberships.title": "还不是任何团队的成员",
"extension.no_memberships.description": "为了分享内容你需要是一个Mattermost服务器上的团队成员。",
"settings.link.error.title": "",
"onboarding.integrations": "",
"onboarding.calls_description": "",
"mobile.calls_participant_rec": "",
"mobile.calls_limit_msg": "",
"mobile.calls_host": "",
"intro.welcome": "",
"display_settings.clock.standard": "",
"channel_info.custom_status": "",
"channel_info.copy_link": "",
"channel_info.copied": "",
"mobile.calls_limit_msg": "每个通话最的参加者人数为 {maxParticipants}。联系您的系统管理员增加人数上限。",
"mobile.calls_host": "主持人",
"intro.welcome": "欢迎来到 {displayName} 频道。",
"display_settings.clock.standard": "12小时",
"channel_info.custom_status": "定制状态:",
"channel_info.copy_link": "复制链接",
"channel_info.copied": "已复制",
"server_upgrade.learn_more": "",
"screen.search.results.file_options.download": "",
"screen.search.results.file_options.copy_link": "",
@ -583,7 +583,7 @@
"mobile.post_info.unsave": "",
"mobile.onboarding.sign_in_to_get_started": "",
"mobile.onboarding.sign_in": "",
"mobile.onboarding.next": "",
"mobile.onboarding.next": "下一个",
"snack.bar.unmute.channel": "",
"snack.bar.message.copied": "",
"snack.bar.favorited.channel": "",
@ -621,52 +621,52 @@
"mobile.post_pre_header.pinned_saved": "",
"notification_settings.email.send": "",
"mobile.search.show_less": "",
"gallery.image_saved": "",
"gallery.image_saved": "图片已保存",
"screens.channel_info": "",
"notification_settings.threads_start": "",
"permalink.error.private_channel_and_team.text": "",
"mobile.calls_error_message": "",
"join_team.error.message": "",
"channel_info.members": "",
"mobile.oauth.failed_to_open_link_no_browser": "",
"mobile.calls_error_message": "错误:{error}",
"join_team.error.message": "在加入团队在出现了一个错误",
"channel_info.members": "成员们",
"mobile.oauth.failed_to_open_link_no_browser": "此链接打开失败。请验证浏览器已经在设备上安装。",
"post.reactions.title": "",
"mobile.search.modifier.phrases": "",
"channel_info.mobile_notifications": "",
"channel_info.mobile_notifications": "移动设备推送",
"settings.notice_text": "",
"mobile.search.modifier.exclude": "",
"share_extension.channel_label": "",
"emoji_picker.recent": "",
"emoji_picker.recent": "最近使用过",
"server.tutorial.swipe": "",
"display_settings.clock.military": "",
"display_settings.tz.auto": "",
"channel_info.mention": "",
"display_settings.clock.military": "24小时",
"display_settings.tz.auto": "自动",
"channel_info.mention": "提及",
"mobile.search.modifier.in": "",
"mobile.request.invalid_request_method": "",
"channel_info.leave_public_channel": "",
"channel_info.leave_public_channel": "您确定离开公开频道{displayName}?您可以重新再次加入。",
"screen.search.results.filter.audio": "",
"mobile.manage_members.manage": "",
"edit_server.title": "",
"invite.summary.sent": "",
"mobile.calls_recording_stop_none_in_progress": "",
"mobile.manage_members.manage": "管理",
"edit_server.title": "编辑服务器名称",
"invite.summary.sent": "您的{sentCount, plural, one {邀请已经} other {邀请已经}} 被发送",
"mobile.calls_recording_stop_none_in_progress": "没有录制正在进行。",
"more_messages.text": "",
"mobile.server_identifier.exists": "",
"download.error": "",
"download.error": "无法下载文件。请稍后重试",
"settings.about.server.version.value": "",
"mobile.calls_call_screen": "",
"mobile.calls_call_screen": "通话",
"user.edit_profile.email.web_client": "",
"share_extension.channel_error": "",
"mobile.search.team.select": "",
"channel_info.muted": "",
"channel_info.muted": "静音",
"post_priority.label.important": "",
"saved_messages.empty.paragraph": "",
"screen.search.results.filter.presentations": "",
"snack.bar.link.copied": "",
"screen.search.results.file_options.open_in_channel": "",
"invite.summary.member_invite": "",
"invite.summary.member_invite": "被邀请为{teamDisplayName}的团队成员",
"user_status.away": "",
"settings_display.timezone.automatically": "",
"screen.search.modifier.header": "",
"invite.summary.try_again": "",
"invite.summary.try_again": "重试",
"mobile.manage_members.change_role.error": "",
"notification_settings.threads_start_participate": "",
"threads.end_of_list.subtitle": "",
@ -704,55 +704,55 @@
"mobile.screen.your_profile": "",
"mobile.screen.settings": "",
"mobile.post_info.save": "",
"mobile.oauth.switch_to_browser.error_title": "",
"mobile.oauth.something_wrong.okButton": "",
"mobile.login_options.separator_text": "",
"mobile.login_options.select_option": "",
"mobile.login_options.openid": "",
"mobile.login_options.office365": "",
"mobile.oauth.switch_to_browser.error_title": "登录错误",
"mobile.oauth.something_wrong.okButton": "好的",
"mobile.login_options.separator_text": "或者 登录以",
"mobile.login_options.select_option": "请在下方选择一个登录选项。",
"mobile.login_options.openid": "Open ID",
"mobile.login_options.office365": "Office 365",
"mobile.login_options.none": "",
"mobile.login_options.heading": "",
"mobile.login_options.google": "",
"mobile.login_options.gitlab": "",
"mobile.login_options.enter_credentials": "",
"mobile.login_options.cant_heading": "",
"mobile.login_options.heading": "登录您的账户",
"mobile.login_options.google": "Google",
"mobile.login_options.gitlab": "GitLab",
"mobile.login_options.enter_credentials": "请在下方输入您登录的信息。",
"mobile.login_options.cant_heading": "无法登录",
"mobile.ios.photos_permission_denied_description": "",
"mobile.edit_post.error": "",
"mobile.edit_post.error": "编辑此消息时发生错误。请再次尝试。",
"mobile.edit_post.delete_question": "",
"mobile.calls_leave_call": "",
"mobile.calls_leave": "",
"mobile.calls_lasted": "",
"mobile.calls_join_call": "",
"mobile.calls_error_title": "",
"mobile.calls_end_msg_channel_default": "",
"mobile.calls_leave_call": "离开通话",
"mobile.calls_leave": "离开",
"mobile.calls_lasted": "通话时间{duration}",
"mobile.calls_join_call": "加入通话",
"mobile.calls_error_title": "错误",
"mobile.calls_end_msg_channel_default": "您确定结束通话?",
"mobile.calls_end_msg_channel": "",
"mobile.calls_end_call_title": "",
"mobile.calls_call_ended": "",
"mobile.calls_end_call_title": "终止通话",
"mobile.calls_call_ended": "通过已结束",
"mobile.android.photos_permission_denied_description": "",
"mobile.add_team.join_team": "",
"mentions.empty.title": "",
"mobile.add_team.join_team": "加入另外的团队",
"mentions.empty.title": "目前没有提及",
"mentions.empty.paragraph": "",
"markdown.latex.error": "",
"login_mfa.token": "",
"login_mfa.enterToken": "",
"load_teams_error.title": "",
"load_teams_error.message": "",
"load_channels_error.title": "",
"load_channels_error.message": "",
"load_categories_error.title": "",
"load_categories_error.message": "",
"intro.welcome.public": "",
"intro.welcome.private": "",
"intro.townsquare": "",
"intro.public_channel": "",
"intro.private_channel": "",
"intro.group_message": "",
"intro.direct_message": "",
"intro.created_by": "",
"intro.channel_info": "",
"intro.add_people": "",
"interactive_dialog.submit": "",
"integration_selector.multiselect.submit": "",
"markdown.latex.error": "Latex渲染错误",
"login_mfa.token": "输入MFA令牌",
"login_mfa.enterToken": "完成此签名流程,请从您移动设备上的认证应用上输入代码。",
"load_teams_error.title": "无法载入{serverName}",
"load_teams_error.message": "在这个服务上有内容载入错误。",
"load_channels_error.title": "无法载入{teamDisplayName}",
"load_channels_error.message": "在这个服务上有内容载入错误。",
"load_categories_error.title": "不能在{serverName}载入类型",
"load_categories_error.message": "在这个服务上有内容载入错误。",
"intro.welcome.public": "加入更多的团队成员到频道里或者从下面开始一个对话。",
"intro.welcome.private": "此非公开频道的消息只有被邀请的成员才能看到。",
"intro.townsquare": "欢迎 {name}。每个人加入团队后自动会成为此频道的成员。",
"intro.public_channel": "公开频道",
"intro.private_channel": "非公开频道",
"intro.group_message": "这是您与此群组开始的对话。消息和文件保存在这里的不会对任何群组外的人显示。",
"intro.direct_message": "这是您与{teammate}开始的对话。消息和文件保存在这里的不会对其他任何人显示。",
"intro.created_by": "由{creator}在{date}创建。",
"intro.channel_info": "信息",
"intro.add_people": "添加新人",
"interactive_dialog.submit": "发送",
"integration_selector.multiselect.submit": "完成",
"screen.search.results.filter.spreadsheets": "",
"your.servers": "",
"video.download": "",
@ -782,9 +782,9 @@
"rate.dont_ask_again": "",
"rate.button.yes": "",
"rate.button.needs_work": "",
"mobile.oauth.switch_to_browser.title": "",
"mobile.integration_selector.loading_options": "",
"mobile.integration_selector.loading_channels": "",
"mobile.oauth.switch_to_browser.title": "切换中…",
"mobile.integration_selector.loading_options": "正在载入选项…",
"mobile.integration_selector.loading_channels": "正在载入频道…",
"post_priority.picker.title": "",
"post_priority.picker.label.urgent": "",
"post_priority.picker.label.standard": "",
@ -797,63 +797,63 @@
"notification_settings.auto_responder": "",
"mobile.server_url.deeplink.emm.denied": "",
"mobile.reset_status.alert_ok": "",
"mobile.oauth.switch_to_browser": "",
"mobile.calls_you": "",
"mobile.calls_viewing_screen": "",
"mobile.calls_more": "",
"mobile.calls_ended_at": "",
"mobile.calls_mic_error": "",
"mobile.oauth.switch_to_browser": "您正在被切换到登录服务提供商",
"mobile.calls_you": "(您)",
"mobile.calls_viewing_screen": "您正在观看{name}的屏幕",
"mobile.calls_more": "更多",
"mobile.calls_ended_at": "结束于",
"mobile.calls_mic_error": "打开设置允许Mattermost访问您的麦克风权限用来加入。",
"notification_settings.push_notification": "",
"notification_settings.ooo_auto_responder": "",
"notification_settings.mobile.trigger_push": "",
"notification_settings.mobile.online": "",
"notification_settings.email": "",
"gallery.copy_link.failed": "",
"find_channels.title": "",
"find_channels.open_dm": "",
"find_channels.new_channel": "",
"find_channels.directory": "",
"gallery.copy_link.failed": "复制到剪切板失败",
"find_channels.title": "查找频道",
"find_channels.open_dm": "打开一个聊天",
"find_channels.new_channel": "新建频道",
"find_channels.directory": "文件夹",
"mobile.display_settings.timezone": "",
"mobile.display_settings.clockDisplay": "",
"mobile.display_settings.clockDisplay": "时钟显示",
"mobile.direct_message.error": "",
"mobile.create_channel.title": "",
"mobile.components.select_server_view.msg_welcome": "",
"mobile.components.select_server_view.msg_description": "",
"mobile.components.select_server_view.msg_connect": "",
"mobile.components.select_server_view.displayName": "",
"mobile.components.select_server_view.displayHelp": "",
"mobile.components.select_server_view.connecting": "",
"mobile.channel_list.unreads": "",
"mobile.channel_list.recent": "",
"mobile.create_channel.title": "新建频道",
"mobile.components.select_server_view.msg_welcome": "欢迎",
"mobile.components.select_server_view.msg_description": "服务器是您的团队通讯的中心它有唯一的URL",
"mobile.components.select_server_view.msg_connect": "让我们连接一个服务器",
"mobile.components.select_server_view.displayName": "显示名",
"mobile.components.select_server_view.displayHelp": "为您的服务器选择一个显示名",
"mobile.components.select_server_view.connecting": "连接中",
"mobile.channel_list.unreads": "未读",
"mobile.channel_list.recent": "最近",
"mobile.camera_photo_permission_denied_description": "",
"mobile.calls_start_call": "",
"mobile.calls_speaker": "",
"mobile.calls_see_logs": "",
"mobile.calls_raise_hand": "",
"mobile.calls_not_available_option": "",
"mobile.calls_not_available_msg": "",
"mobile.calls_noone_talking": "",
"mobile.calls_name_started_call": "",
"mobile.calls_name_is_talking": "",
"mobile.calls_mute": "",
"gallery.unsupported": "",
"gallery.save_failed": "",
"gallery.preparing": "",
"display_settings.tz.manual": "",
"display_settings.timezone": "",
"display_settings.theme": "",
"display_settings.clockDisplay": "",
"create_post.thread_reply": "",
"channel_modal.makePrivate.label": "",
"channel_modal.makePrivate.description": "",
"channel_info.unarchive_failed": "",
"channel_info.unarchive_description": "",
"channel_info.unarchive": "",
"channel_info.set_header": "",
"channel_info.send_mesasge": "",
"channel_info.convert_private_title": "",
"channel_info.convert_private_success": "",
"channel_info.convert_private_description": "",
"mobile.calls_start_call": "开始通话",
"mobile.calls_speaker": "喇叭",
"mobile.calls_see_logs": "查是服务器日志",
"mobile.calls_raise_hand": "举手",
"mobile.calls_not_available_option": "(当前无效)",
"mobile.calls_not_available_msg": "请联系您的系统管理员打开此功能。",
"mobile.calls_noone_talking": "没有人发言",
"mobile.calls_name_started_call": "{name}开启了通话",
"mobile.calls_name_is_talking": "{name}正在说话",
"mobile.calls_mute": "静音",
"gallery.unsupported": "这个文件类型不支持预览。尝试下载或者分享到另外一个应用打开。",
"gallery.save_failed": "无法保存文件",
"gallery.preparing": "准备中…",
"display_settings.tz.manual": "手动",
"display_settings.timezone": "时区",
"display_settings.theme": "主题",
"display_settings.clockDisplay": "时钟显示",
"create_post.thread_reply": "回复此话题…",
"channel_modal.makePrivate.label": "设为非公开",
"channel_modal.makePrivate.description": "当一个频道被设置成非公开,只有被邀请的团队成员能够访问和参与频道",
"channel_info.unarchive_failed": "在尝试取消归档频道{displayName}时发生错误",
"channel_info.unarchive_description": "您确定要取消归档 {term} {name}",
"channel_info.unarchive": "取消归档频道",
"channel_info.set_header": "设置标题",
"channel_info.send_mesasge": "发送消息",
"channel_info.convert_private_title": "转变 {displayName} 为非公开频道?",
"channel_info.convert_private_success": "{displayName} 现在为非公开频道。",
"channel_info.convert_private_description": "当你转变{displayName}成一个非公开频道,历史和关联关系将会被保留。公共分享文件的链接所有人都可以继续访问。私有频道只能通过邀请加入。\n\n此更改是永久的而且不能撤销。\n\n您确定将{displayName}转变成一个非公开频道吗?",
"select_team.description": "",
"screens.channel_edit": "",
"screen.search.title": "",
@ -870,54 +870,54 @@
"notification_settings.push_threads.replies": "",
"snack.bar.undo": "",
"settings.save": "",
"edit_server.save": "",
"edit_server.display_help": "",
"edit_server.description": "",
"custom_status.suggestions.title": "",
"custom_status.suggestions.recent_title": "",
"create_direct_message.title": "",
"channel_modal.purposeEx": "",
"channel_modal.nameEx": "",
"channel_modal.headerHelp": "",
"channel_modal.headerEx": "",
"channel_list.find_channels": "",
"channel_list.favorites_category": "",
"channel_list.dms_category": "",
"channel_list.channels_category": "",
"channel_intro.createdOn": "",
"channel_intro.createdBy": "",
"channel_info.unarchive_title": "",
"channel_info.send_a_mesasge": "",
"channel_info.public_channel": "",
"channel_info.private_channel": "",
"channel_info.position": "",
"channel_info.pinned_messages": "",
"channel_info.notification.none": "",
"channel_info.notification.default": "",
"channel_info.notification.all": "",
"channel_info.nickname": "",
"channel_info.local_time": "",
"channel_info.leave_private_channel": "",
"channel_info.leave_channel": "",
"channel_info.leave": "",
"channel_info.ignore_mentions": "",
"channel_info.favorited": "",
"channel_info.favorite": "",
"channel_info.error_close": "",
"channel_info.edit_header": "",
"channel_info.convert_private": "",
"channel_info.convert_failed": "",
"channel_info.close_gm_channel": "",
"channel_info.close_gm": "",
"channel_info.close_dm": "",
"channel_info.close": "",
"channel_info.archive_title": "",
"channel_info.archive_failed": "",
"edit_server.save": "保存",
"edit_server.display_help": "服务器:{url}",
"edit_server.description": "为此服务器指定一个显示名称",
"custom_status.suggestions.title": "建议",
"custom_status.suggestions.recent_title": "最近",
"create_direct_message.title": "创建聊天",
"channel_modal.purposeEx": "用来提交bug和改进的频道",
"channel_modal.nameEx": "Bugs, 市场",
"channel_modal.headerHelp": "设定在频道标题里在频道旁边的文字。举例,输入常见链接 [链接标题](http://example.com)。",
"channel_modal.headerEx": "使用Markdown格式化标题",
"channel_list.find_channels": "寻找频道...",
"channel_list.favorites_category": "收藏",
"channel_list.dms_category": "聊天",
"channel_list.channels_category": "频道",
"channel_intro.createdOn": "{date}创建",
"channel_intro.createdBy": "由{user}在{date}创建",
"channel_info.unarchive_title": "取消归档 {term}",
"channel_info.send_a_mesasge": "发送消息",
"channel_info.public_channel": "公共频道",
"channel_info.private_channel": "私有频道",
"channel_info.position": "职位",
"channel_info.pinned_messages": "标注消息",
"channel_info.notification.none": "从不",
"channel_info.notification.default": "默认",
"channel_info.notification.all": "全部",
"channel_info.nickname": "昵称",
"channel_info.local_time": "两地时间",
"channel_info.leave_private_channel": "您确定离开非公开频道{displayName}?您将不能加入频道除非你被再次邀请。",
"channel_info.leave_channel": "离开频道",
"channel_info.leave": "离开",
"channel_info.ignore_mentions": "忽略 @channel、@here 以及 @all",
"channel_info.favorited": "已收藏",
"channel_info.favorite": "收藏",
"channel_info.error_close": "关闭",
"channel_info.edit_header": "编辑标题",
"channel_info.convert_private": "转成非公开频道",
"channel_info.convert_failed": "我们无法将{displayName} 转变为私有频道。",
"channel_info.close_gm_channel": "您确定关闭此对话吗?会话将从主屏幕中删除,但您可以再次打开它。",
"channel_info.close_gm": "关闭群聊",
"channel_info.close_dm": "关闭对话",
"channel_info.close": "关闭",
"channel_info.archive_title": "归档{term}",
"channel_info.archive_failed": "在试图归档频道{displayName}时发生错误",
"channel_info.archive": "归档频道",
"channel_info.alertYes": "是",
"channel_info.alertNo": "不",
"channel_info.alert_retry": "重试",
"channel_header.member_count": "",
"channel_header.member_count": "{count, plural, one {# 成员} other {# 成员}}",
"channel_header.info": "频道信息",
"channel_header.directchannel.you": "{displayName} (您)",
"browse_channels.title": "浏览频道",
@ -961,36 +961,48 @@
"notification_settings.mentions": "",
"notification_settings.mention.reply": "",
"notification_settings.email.crt.emailInfo": "",
"mobile.calls_limit_reached": "",
"mobile.calls_end_permission_title": "",
"mobile.calls_end_permission_msg": "",
"mobile.calls_enable": "",
"mobile.calls_disable": "",
"mobile.calls_current_call": "",
"mobile.calls_limit_reached": "达到参加者上限",
"mobile.calls_end_permission_title": "错误",
"mobile.calls_end_permission_msg": "您没有权限结束通话。请联系通话创建者结束此通话。",
"mobile.calls_enable": "启动通话",
"mobile.calls_disable": "无效的通话",
"mobile.calls_current_call": "目前的通话",
"user.edit_profile.email.auth_service": "",
"settings.notice_mobile_link": "",
"settings.advanced_settings": "",
"settings.about.database": "",
"mobile.post_pre_header.saved": "",
"mobile.participants.header": "",
"mobile.no_results.spelling": "",
"mobile.no_results_with_term.messages": "",
"mobile.no_results_with_term.files": "",
"mobile.no_results_with_term": "",
"mobile.login_options.saml": "",
"mobile.leave_and_join_title": "",
"mobile.no_results.spelling": "检查拼写或者尝试另外的搜索。",
"mobile.no_results_with_term.messages": "\"{term}\"没有匹配的发现",
"mobile.no_results_with_term.files": "\"{term}\"没有匹配的文件",
"mobile.no_results_with_term": "“{term}” 没有相关结果",
"mobile.login_options.saml": "SAML",
"mobile.leave_and_join_title": "您确定切换到另一个通话吗?",
"mobile.leave_and_join_message": "",
"mobile.leave_and_join_confirmation": "",
"mobile.join_channel.error": "",
"mobile.create_post.read_only": "",
"mobile.calls_ok": "",
"home.header.plus_menu": "",
"global_threads.options.title": "",
"general_settings.notifications": "",
"general_settings.help": "",
"general_settings.display": "",
"general_settings.advanced_settings": "",
"general_settings.about": "",
"gallery.video_saved": "",
"gallery.downloading": ""
"mobile.leave_and_join_confirmation": "离开 & 加入",
"mobile.join_channel.error": "我们不会参加频道{displayName}。",
"mobile.create_post.read_only": "此频道只读。",
"mobile.calls_ok": "好的",
"home.header.plus_menu": "选项",
"global_threads.options.title": "话题操作",
"general_settings.notifications": "通知",
"general_settings.help": "帮助",
"general_settings.display": "显示",
"general_settings.advanced_settings": "高级设定",
"general_settings.about": "关于{appTitle}",
"gallery.video_saved": "视频已保存",
"gallery.downloading": "下载中…",
"channel_notification_preferences.unmute_content": "取消频道静音",
"channel_notification_preferences.thread_replies": "话题回复",
"channel_notification_preferences.reset_default": "重设为默认",
"channel_notification_preferences.notify_about": "通知我当…",
"channel_notification_preferences.notification.thread_replies": "此频道我关注的话题有回复时通知我",
"channel_notification_preferences.notification.none": "无",
"channel_notification_preferences.notification.mention": "仅提及,聊天",
"channel_notification_preferences.notification.all": "所有新消息",
"channel_notification_preferences.muted_title": "此频道被已静音",
"channel_notification_preferences.muted_content": "您可以改变通知设置,但是在频道被取消静音前你将不会接受到通知。",
"channel_notification_preferences.default": "(默认)",
"invite.summary.back": "向后"
}

View file

@ -8,8 +8,8 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.721.0)
aws-sdk-core (3.170.0)
aws-partitions (1.735.0)
aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@ -17,7 +17,7 @@ GEM
aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.119.1)
aws-sdk-s3 (1.119.2)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -111,7 +111,7 @@ GEM
fastlane-plugin-find_replace_string (0.1.0)
fastlane-plugin-versioning_android (0.1.1)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.35.0)
google-apis-androidpublisher_v3 (0.37.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
@ -142,7 +142,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.3.0)
googleauth (1.5.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)

View file

@ -1128,7 +1128,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 461;
CURRENT_PROJECT_VERSION = 463;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1172,7 +1172,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 461;
CURRENT_PROJECT_VERSION = 463;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1315,7 +1315,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 461;
CURRENT_PROJECT_VERSION = 463;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
@ -1366,7 +1366,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 461;
CURRENT_PROJECT_VERSION = 463;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>461</string>
<string>463</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<key>CFBundleVersion</key>
<string>461</string>
<string>463</string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>

View file

@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<key>CFBundleVersion</key>
<string>461</string>
<string>463</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View file

@ -500,7 +500,7 @@ PODS:
- React-perflogger (= 0.71.3)
- ReactNativeExceptionHandler (2.10.10):
- React-Core
- ReactNativeIncallManager (4.0.0):
- ReactNativeIncallManager (4.0.1):
- React-Core
- ReactNativeKeyboardTrackingView (5.7.0):
- React
@ -999,7 +999,7 @@ SPEC CHECKSUMS:
React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e
ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40
ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60
ReactNativeIncallManager: b169b57d3064d8f62478f8fc3c485da6c75045d1
ReactNativeIncallManager: 0d2cf9f4d50359728a30c08549762fe67a2efb81
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
ReactNativeNavigation: d79d9d53e6025851936bb8b3d13760b86302a669
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd

View file

@ -19,6 +19,6 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/file_transformer.js',
},
transformIgnorePatterns: [
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch)',
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch|@mattermost/calls)',
],
};

47
package-lock.json generated
View file

@ -17,6 +17,7 @@
"@formatjs/intl-pluralrules": "5.1.8",
"@formatjs/intl-relativetimeformat": "11.1.8",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/calls": "github:mattermost/calls-common#v0.12.0",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.2",
@ -72,7 +73,7 @@
"react-native-hw-keyboard-event": "0.0.4",
"react-native-image-picker": "5.0.2",
"react-native-in-app-review": "4.2.1",
"react-native-incall-manager": "github:cpoile/react-native-incall-manager",
"react-native-incall-manager": "4.0.1",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-keyboard-tracking-view": "5.7.0",
"react-native-keychain": "8.1.1",
@ -124,6 +125,7 @@
"@types/jest": "29.4.0",
"@types/lodash": "4.14.191",
"@types/mime-db": "1.43.1",
"@types/pako": "2.0.0",
"@types/querystringify": "2.0.0",
"@types/react": "18.0.28",
"@types/react-native-background-timer": "2.0.0",
@ -3195,6 +3197,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@mattermost/calls": {
"name": "@calls/common",
"version": "0.12.0",
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#02c52be3b71080023803621877ab96a7fe306fba"
},
"node_modules/@mattermost/commonmark": {
"version": "0.30.1-0",
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-0.tgz",
@ -5957,6 +5964,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
},
"node_modules/@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -18461,8 +18474,8 @@
"integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ=="
},
"node_modules/react-native-incall-manager": {
"version": "4.0.0",
"resolved": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e",
"version": "4.0.1",
"resolved": "git+ssh://git@github.com/react-native-webrtc/react-native-incall-manager.git#6d927ef24c6e47c6134177a4bb14a71054f85b65",
"license": "ISC",
"peerDependencies": {
"react-native": ">=0.40.0"
@ -21582,9 +21595,9 @@
}
},
"node_modules/webpack": {
"version": "5.74.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
"integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==",
"version": "5.76.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz",
"integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==",
"dev": true,
"peer": true,
"dependencies": {
@ -24264,6 +24277,10 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@mattermost/calls": {
"version": "git+ssh://git@github.com/mattermost/calls-common.git#02c52be3b71080023803621877ab96a7fe306fba",
"from": "@mattermost/calls@github:mattermost/calls-common#v0.12.0"
},
"@mattermost/commonmark": {
"version": "0.30.1-0",
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-0.tgz",
@ -26264,7 +26281,7 @@
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "^18.0.15",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
@ -26328,6 +26345,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
},
"@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -35891,8 +35914,8 @@
"integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ=="
},
"react-native-incall-manager": {
"version": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e",
"from": "react-native-incall-manager@github:cpoile/react-native-incall-manager",
"version": "git+ssh://git@github.com/react-native-webrtc/react-native-incall-manager.git#6d927ef24c6e47c6134177a4bb14a71054f85b65",
"from": "react-native-incall-manager@4.0.1",
"requires": {}
},
"react-native-iphone-x-helper": {
@ -38180,9 +38203,9 @@
}
},
"webpack": {
"version": "5.74.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
"integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==",
"version": "5.76.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz",
"integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==",
"dev": true,
"peer": true,
"requires": {

View file

@ -14,6 +14,7 @@
"@formatjs/intl-pluralrules": "5.1.8",
"@formatjs/intl-relativetimeformat": "11.1.8",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/calls": "github:mattermost/calls-common#v0.12.0",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.2",
@ -69,7 +70,7 @@
"react-native-hw-keyboard-event": "0.0.4",
"react-native-image-picker": "5.0.2",
"react-native-in-app-review": "4.2.1",
"react-native-incall-manager": "github:cpoile/react-native-incall-manager",
"react-native-incall-manager": "4.0.1",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-keyboard-tracking-view": "5.7.0",
"react-native-keychain": "8.1.1",
@ -121,6 +122,7 @@
"@types/jest": "29.4.0",
"@types/lodash": "4.14.191",
"@types/mime-db": "1.43.1",
"@types/pako": "2.0.0",
"@types/querystringify": "2.0.0",
"@types/react": "18.0.28",
"@types/react-native-background-timer": "2.0.0",

View file

@ -162,6 +162,34 @@ index a34598c..b035a76 100644
if (navigationActivity != null) {
navigationActivity.onCatalystInstanceDestroy();
}
diff --git a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt
index a79e487..dba9b65 100644
--- a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt
+++ b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt
@@ -3,11 +3,20 @@ package com.reactnativenavigation.utils
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
+import android.view.ViewGroup
val hitRect = Rect()
fun MotionEvent.coordinatesInsideView(view: View?): Boolean {
view ?: return false
- view.getHitRect(hitRect)
- return hitRect.contains(x.toInt(), y.toInt())
+ val viewGroup = (view as ViewGroup).getChildAt(0) as ViewGroup
+ return if (viewGroup.childCount > 0) {
+ val content = viewGroup.getChildAt(0)
+
+ content.getHitRect(hitRect)
+
+ hitRect.contains(x.toInt(), y.toInt())
+ } else {
+ false
+ }
}
\ No newline at end of file
diff --git a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java b/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java
index 035ec31..630b8d4 100644
--- a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java

View file

@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Type definitions for react-native-incall-manager 3.2
// Project: https://github.com/zxcpoiu/react-native-incall-manager#readme
// Definitions by: Carlos Quiroga <https://github.com/KarlosQ>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
declare module 'react-native-incall-manager' {
export interface StartSetup {
media?: string | undefined;
auto?: boolean | undefined;
ringback?: string | undefined;
}
export interface StopSetup {
busytone?: string | undefined;
}
export class InCallManager {
start(setup?: StartSetup): void;
stop(setup?: StopSetup): void;
turnScreenOff(): void;
turnScreenOn(): void;
getIsWiredHeadsetPluggedIn(): Promise<any>;
setFlashOn(enable?: boolean, brightness?: number): number;
setKeepScreenOn(enable?: boolean): void;
setSpeakerphoneOn(enable?: boolean): void;
setForceSpeakerphoneOn(_flag?: boolean): void;
setMicrophoneMute(enable?: boolean): void;
startRingtone(
ringtone?: string,
vibrate_pattern?: any[],
ios_category?: string,
seconds?: number
): void;
stopRingtone(): void;
startProximitySensor(): void;
stopProximitySensor(): void;
startRingback(ringback?: string): void;
stopRingback(): void;
pokeScreen(_timeout?: number): void;
getAudioUri(audioType: string, fileType: string): any;
chooseAudioRoute(route: any): any;
requestAudioFocus(): void;
abandonAudioFocus(): void;
}
declare const _default: InCallManager;
export default _default;
}