Compare commits

..

13 Commits

Author SHA1 Message Date
Christopher Poile
2bcea5a6a3 use calls/common rtcpeer; @calls -> @mmcalls 2023-03-06 11:42:13 -05:00
Elisabeth Kulzer
6454a19a37 Detox: Android - fix invite ppl. (#7162)
* Detox: Android - fix invite ppl.

* Remove config modifications.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2023-03-03 19:04:19 +01:00
Pantelis Vratsalis
71805ed79d Bump app build number to 460 2023-03-03 17:37:48 +02:00
Elias Nahum
fe916ec740 Refactor category channels to react to setting changes and apply the correct order (#7170)
* Refactor category channels to react to setting changes and apply the correct order

* feedback review
2023-03-03 15:53:29 +02:00
Elias Nahum
3c046ae39c Fix push notification token registration race/missing (#7183) 2023-03-03 12:14:08 +02:00
Elias Nahum
a804a7331f support WS connection over TLS1.3 (#7182)
* support WS connection over TLS1.3

* fix updateDraftMessage on unmount
2023-03-03 11:33:01 +02:00
Elias Nahum
903aaf62b5 Fix display name when open own DM (#7181) 2023-03-02 16:57:49 +02:00
Elias Nahum
ef4fb9c8e0 fix entry for tablets (#7179) 2023-03-02 16:52:54 +02:00
Elias Nahum
af07f511f7 use sourceScreen instead of location in post options (#7176) 2023-03-02 12:46:18 +02:00
Elias Nahum
abd388986f trigger Search when hardware keyboard enter key is pressed (#7174) 2023-03-01 14:38:23 +02:00
Elias Nahum
0938045b7d Fix potential reaction crash (#7172) 2023-03-01 13:24:11 +02:00
Elias Nahum
9347e736e5 ignore leading and trailing spaces when editing profile (#7173) 2023-03-01 13:22:35 +02:00
Elias Nahum
276bcba956 Fix iOS push notification when set as generic message with sender name (#7171) 2023-03-01 12:59:07 +02:00
35 changed files with 182 additions and 762 deletions

View File

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

View File

@@ -6,7 +6,7 @@ import {Platform} from 'react-native';
import {WebsocketEvents} from '@constants';
import DatabaseManager from '@database/manager';
import {getConfigValue} from '@queries/servers/system';
import {getConfig} from '@queries/servers/system';
import {hasReliableWebsocket} from '@utils/config';
import {toMilliseconds} from '@utils/datetime';
import {logError, logInfo, logWarning} from '@utils/log';
@@ -79,12 +79,8 @@ export default class WebSocketClient {
return;
}
const [websocketUrl, version, reliableWebsocketConfig] = await Promise.all([
getConfigValue(database, 'WebsocketURL'),
getConfigValue(database, 'Version'),
getConfigValue(database, 'EnableReliableWebSockets'),
]);
const connectionUrl = (websocketUrl || this.serverUrl) + '/api/v4/websocket';
const config = await getConfig(database);
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
if (this.connectingCallback) {
this.connectingCallback();
@@ -105,7 +101,7 @@ export default class WebSocketClient {
this.url = connectionUrl;
const reliableWebSockets = hasReliableWebsocket(version, reliableWebsocketConfig);
const reliableWebSockets = hasReliableWebsocket(config);
if (reliableWebSockets) {
// Add connection id, and last_sequence_number to the query param.
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
@@ -133,11 +129,6 @@ export default class WebSocketClient {
headers.Authorization = `Bearer ${this.token}`;
}
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT});
// Check again if the client is the same, to avoid race conditions
if (this.conn === client) {
return;
}
this.conn = client;
} catch (error) {
return;

View File

@@ -14,7 +14,7 @@ import TeamList from './team_list';
type Props = {
iconPad?: boolean;
canJoinOtherTeams: boolean;
hasMoreThanOneTeam: boolean;
teamsCount: number;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -36,8 +36,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneTeam}: Props) {
const initialWidth = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) {
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
const width = useSharedValue(initialWidth);
const marginTop = useSharedValue(iconPad ? 44 : 0);
const theme = useTheme();
@@ -58,8 +58,8 @@ export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneT
}, [iconPad]);
useEffect(() => {
width.value = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
}, [hasMoreThanOneTeam]);
width.value = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
}, [teamsCount]);
return (
<Animated.View style={[styles.container, transform]}>

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 '@mmcalls/common/lib/types';
import type {IntlShape} from 'react-intl';
let connection: CallsConnection | null = null;
@@ -322,7 +321,7 @@ export const unraiseHand = () => {
}
};
export const sendReaction = (emoji: CallReactionEmoji) => {
export const sendReaction = (emoji: EmojiData) => {
if (connection) {
connection.sendReaction(emoji);
}
@@ -415,7 +414,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 +432,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 '@mmcalls/common/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 '@mmcalls/common/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

@@ -1,18 +1,17 @@
// 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 '@mmcalls/common/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';
@@ -22,7 +21,9 @@ import {logError, logDebug, logWarning} 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 {RTCPeerOpts} from '@mmcalls/common/lib/rtc_peer';
import type {EmojiData} from '@mmcalls/common/lib/types';
const peerConnectTimeout = 5000;
@@ -164,7 +165,7 @@ export async function newConnection(
}
};
const sendReaction = (emoji: CallReactionEmoji) => {
const sendReaction = (emoji: EmojiData) => {
if (ws) {
ws.send('react', {
data: JSON.stringify(emoji),
@@ -204,7 +205,14 @@ export async function newConnection(
InCallManager.start({media: 'video'});
setSpeakerPhone(true);
peer = new RTCPeer({iceServers: iceConfigs || []});
const opts: RTCPeerOpts = {
logDebug,
webrtc: {
MediaStream,
RTCPeerConnection,
},
};
peer = new RTCPeer({iceServers: iceConfigs || []}, opts);
peer.on('offer', (sdp) => {
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);

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

@@ -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 '@mmcalls/common/lib/types';
jest.mock('@calls/alerts');
@@ -797,6 +797,7 @@ describe('useCallsState', () => {
it('config', () => {
const newConfig = {
...DefaultCallsConfig,
ICEServers: [],
ICEServersConfigs: [
{
@@ -914,7 +915,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 '@mmcalls/common/lib/types';
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
const channelsWithCalls = Object.keys(calls).reduce(
(accum, next) => {
@@ -390,7 +390,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 +423,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.
@@ -474,7 +474,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;
@@ -498,7 +498,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 '@mmcalls/common/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,18 +141,6 @@ export type ApiResp = {
status_code: number;
}
export type CallReactionEmoji = {
name: string;
skin?: string;
unified: string;
}
export type CallReaction = {
user_id: string;
emoji: CallReactionEmoji;
timestamp: number;
}
export type ReactionStreamEmoji = {
name: string;
latestTimestamp: number;

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 '@mmcalls/common/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) {
@@ -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

@@ -3,7 +3,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$, Observable} from 'rxjs';
import {of as of$} from 'rxjs';
import {switchMap, combineLatestWith, distinctUntilChanged} from 'rxjs/operators';
import {Preferences} from '@constants';
@@ -19,7 +19,6 @@ import CategoryBody from './category_body';
import type {WithDatabaseArgs} from '@typings/database/database';
import type CategoryModel from '@typings/database/models/servers/category';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type PreferenceModel from '@typings/database/models/servers/preference';
type EnhanceProps = {
@@ -31,7 +30,8 @@ type EnhanceProps = {
const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
const observeCategoryChannels = (category: CategoryModel, myChannels: Observable<MyChannelModel[]>) => {
const observeCategoryChannels = (category: CategoryModel) => {
const myChannels = category.myChannels.observeWithColumns(['last_post_at', 'is_unread']);
const channels = category.channels.observeWithColumns(['create_at', 'display_name']);
const manualSort = category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']);
return myChannels.pipe(
@@ -57,8 +57,7 @@ const observeCategoryChannels = (category: CategoryModel, myChannels: Observable
};
const enhanced = withObservables([], ({category, currentUserId, database, isTablet, locale}: EnhanceProps) => {
const categoryMyChannels = category.myChannels.observeWithColumns(['last_post_at', 'is_unread']);
const channelsWithMyChannel = observeCategoryChannels(category, categoryMyChannels);
const channelsWithMyChannel = observeCategoryChannels(category);
const currentChannelId = isTablet ? observeCurrentChannelId(database) : of$('');
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
@@ -78,9 +77,9 @@ const enhanced = withObservables([], ({category, currentUserId, database, isTabl
);
}
const notifyPropsPerChannel = categoryMyChannels.pipe(
const notifyPropsPerChannel = channelsWithMyChannel.pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((mc) => observeNotifyPropsByChannels(database, mc)),
switchMap((cwms) => observeNotifyPropsByChannels(database, cwms.map((c) => c.myChannel))),
);
const hiddenDmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').

View File

@@ -4,7 +4,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {switchMap} from 'rxjs/operators';
import {Permissions} from '@constants';
import {observePermissionForTeam} from '@queries/servers/role';
@@ -25,7 +25,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const canJoinChannels = combineLatest([currentUser, team]).pipe(
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
distinctUntilChanged(),
);
const canCreatePublicChannels = combineLatest([currentUser, team]).pipe(
@@ -38,7 +37,6 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe(
switchMap(([open, priv]) => of$(open || priv)),
distinctUntilChanged(),
);
const canAddUserToTeam = combineLatest([currentUser, team]).pipe(
@@ -50,11 +48,9 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
canJoinChannels,
canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe(
switchMap(([openServer, addUser]) => of$(openServer && addUser)),
distinctUntilChanged(),
),
displayName: team.pipe(
switchMap((t) => of$(t?.displayName)),
distinctUntilChanged(),
),
pushProxyStatus: observePushVerificationStatus(database),
};

View File

@@ -33,8 +33,8 @@ describe('components/categories_list', () => {
it('should render', () => {
const wrapper = renderWithEverything(
<CategoriesList
moreThanOneTeam={false}
hasChannels={true}
teamsCount={1}
channelsCount={1}
/>,
{database},
);
@@ -46,8 +46,8 @@ describe('components/categories_list', () => {
const wrapper = renderWithEverything(
<CategoriesList
isCRTEnabled={true}
moreThanOneTeam={false}
hasChannels={true}
teamsCount={1}
channelsCount={1}
/>,
{database},
);
@@ -67,8 +67,8 @@ describe('components/categories_list', () => {
jest.useFakeTimers();
const wrapper = renderWithEverything(
<CategoriesList
moreThanOneTeam={false}
hasChannels={true}
teamsCount={0}
channelsCount={1}
/>,
{database},
);
@@ -89,8 +89,8 @@ describe('components/categories_list', () => {
jest.useFakeTimers();
const wrapper = renderWithEverything(
<CategoriesList
moreThanOneTeam={true}
hasChannels={false}
teamsCount={1}
channelsCount={0}
/>,
{database},
);

View File

@@ -27,28 +27,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
}));
type ChannelListProps = {
hasChannels: boolean;
channelsCount: number;
iconPad?: boolean;
isCRTEnabled?: boolean;
moreThanOneTeam: boolean;
teamsCount: number;
};
const getTabletWidth = (moreThanOneTeam: boolean) => {
return TABLET_SIDEBAR_WIDTH - (moreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0);
const getTabletWidth = (teamsCount: number) => {
return TABLET_SIDEBAR_WIDTH - (teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0);
};
const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: ChannelListProps) => {
const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: ChannelListProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const {width} = useWindowDimensions();
const isTablet = useIsTablet();
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(moreThanOneTeam) : 0);
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(teamsCount) : 0);
useEffect(() => {
if (isTablet) {
tabletWidth.value = getTabletWidth(moreThanOneTeam);
tabletWidth.value = getTabletWidth(teamsCount);
}
}, [isTablet && moreThanOneTeam]);
}, [isTablet && teamsCount]);
const tabletStyle = useAnimatedStyle(() => {
if (!isTablet) {
@@ -61,7 +61,7 @@ const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: C
}, [isTablet, width]);
const content = useMemo(() => {
if (!hasChannels) {
if (channelsCount < 1) {
return (<LoadChannelsError/>);
}

View File

@@ -29,10 +29,9 @@ import Servers from './servers';
import type {LaunchType} from '@typings/launch';
type ChannelProps = {
hasChannels: boolean;
channelsCount: number;
isCRTEnabled: boolean;
hasTeams: boolean;
hasMoreThanOneTeam: boolean;
teamsCount: number;
isLicensed: boolean;
showToS: boolean;
launchType: LaunchType;
@@ -127,10 +126,10 @@ const ChannelListScreen = (props: ChannelProps) => {
}, [theme, insets.top]);
useEffect(() => {
if (!props.hasTeams) {
if (!props.teamsCount) {
resetToTeams();
}
}, [Boolean(props.hasTeams)]);
}, [Boolean(props.teamsCount)]);
useEffect(() => {
const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
@@ -177,13 +176,13 @@ const ChannelListScreen = (props: ChannelProps) => {
>
<TeamSidebar
iconPad={canAddOtherServers}
hasMoreThanOneTeam={props.hasMoreThanOneTeam}
teamsCount={props.teamsCount}
/>
<CategoriesList
iconPad={canAddOtherServers && !props.hasMoreThanOneTeam}
iconPad={canAddOtherServers && props.teamsCount <= 1}
isCRTEnabled={props.isCRTEnabled}
moreThanOneTeam={props.hasMoreThanOneTeam}
hasChannels={props.hasChannels}
teamsCount={props.teamsCount}
channelsCount={props.channelsCount}
/>
{isTablet &&
<AdditionalTabletView/>

View File

@@ -4,7 +4,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {switchMap} from 'rxjs/operators';
import {queryAllMyChannelsForTeam} from '@queries/servers/channel';
import {observeCurrentTeamId, observeLicense} from '@queries/servers/system';
@@ -21,22 +21,11 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap((lcs) => (lcs ? of$(lcs.IsLicensed === 'true') : of$(false))),
);
const teamsCount = queryMyTeams(database).observeCount(false);
return {
isCRTEnabled: observeIsCRTEnabled(database),
hasTeams: teamsCount.pipe(
switchMap((v) => of$(v > 0)),
distinctUntilChanged(),
),
hasMoreThanOneTeam: teamsCount.pipe(
switchMap((v) => of$(v > 1)),
distinctUntilChanged(),
),
hasChannels: observeCurrentTeamId(database).pipe(
teamsCount: queryMyTeams(database).observeCount(false),
channelsCount: observeCurrentTeamId(database).pipe(
switchMap((id) => (id ? queryAllMyChannelsForTeam(database, id).observeCount(false) : of$(0))),
switchMap((v) => of$(v > 0)),
distinctUntilChanged(),
),
isLicensed,
showToS: observeShowToS(database),

View File

@@ -3,7 +3,7 @@
import {General, Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {getPreferenceAsBool, getPreferenceValue} from '@helpers/api/preference';
import {isDMorGM} from '@utils/channel';
import {getUserIdFromChannelName} from '@utils/user';
@@ -42,18 +42,15 @@ export const filterAutoclosedDMs = (
// Only autoclose DMs that haven't been assigned to a category
return channelsWithMyChannel;
}
const prefMap = preferences.reduce((acc, v) => {
const existing = acc.get(v.name);
acc.set(v.name, Math.max((v.value as unknown as number) || 0, existing || 0));
return acc;
}, new Map<string, number>());
const getLastViewedAt = (cwm: ChannelWithMyChannel) => {
// The server only ever sets the last_viewed_at to the time of the last post in channel, so we may need
// to use the preferences added for the previous version of autoclosing DMs.
const id = cwm.channel.id;
return Math.max(
cwm.myChannel.lastViewedAt,
prefMap.get(id) || 0,
getPreferenceValue<number>(preferences, Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, id, 0),
getPreferenceValue<number>(preferences, Preferences.CATEGORIES.CHANNEL_OPEN_TIME, id, 0),
);
};
@@ -181,7 +178,7 @@ export const sortChannels = (sorting: CategorySorting, channelsWithMyChannel: Ch
}).map((cwm) => cwm.channel);
} else if (sorting === 'manual') {
return channelsWithMyChannel.sort((cwmA, cwmB) => {
return cwmA.sortOrder - cwmB.sortOrder;
return cwmB.sortOrder - cwmA.sortOrder;
}).map((cwm) => cwm.channel);
}

View File

@@ -3,10 +3,10 @@
import {isMinimumServerVersion} from './helpers';
export function hasReliableWebsocket(version?: string, reliableWebsocketsConfig?: string) {
if (version && isMinimumServerVersion(version, 6, 5)) {
export function hasReliableWebsocket(config: ClientConfig) {
if (isMinimumServerVersion(config.Version, 6, 5)) {
return true;
}
return reliableWebsocketsConfig === 'true';
return config.EnableReliableWebSockets === 'true';
}

View File

@@ -1,305 +0,0 @@
{
"ServiceSettings": {
"SiteURL": "http://localhost:8065",
"WebsocketURL": "",
"LicenseFileLocation": "",
"ListenAddress": ":8065",
"ConnectionSecurity": "",
"TLSCertFile": "",
"TLSKeyFile": "",
"TLSMinVer": "1.2",
"TLSStrictTransport": false,
"TLSStrictTransportMaxAge": 63072000,
"TLSOverwriteCiphers": [],
"UseLetsEncrypt": false,
"Forward80To443": false,
"TrustedProxyIPHeader": [],
"ReadTimeout": 300,
"WriteTimeout": 300,
"IdleTimeout": 300,
"MaximumLoginAttempts": 10,
"GoroutineHealthThreshold": -1,
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnableCommands": true,
"EnableOnlyAdminIntegrations": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableLinkPreviews": false,
"EnableTesting": false,
"EnableDeveloper": false,
"EnableOpenTracing": false,
"EnableSecurityFixAlert": true,
"EnableInsecureOutgoingConnections": false,
"AllowedUntrustedInternalConnections": "localhost",
"EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
"CorsExposedHeaders": "",
"CorsAllowCredentials": false,
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"ExtendSessionLengthWithActivity": true,
"SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30,
"SessionLengthSSOInDays": 30,
"SessionCacheInMinutes": 10,
"SessionIdleTimeoutInMinutes": 43200,
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"EnableGifPicker": false,
"GfycatApiKey": "2_KtH_W5",
"GfycatApiSecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof",
"RestrictCustomEmojiCreation": "all",
"RestrictPostDelete": "all",
"AllowEditPost": "always",
"PostEditTimeLimit": -1,
"TimeBetweenUserTypingUpdatesMilliseconds": 5000,
"EnablePostSearch": true,
"MinimumHashtagLength": 3,
"EnableUserTypingMessages": true,
"EnableChannelViewedMessages": true,
"EnableUserStatuses": true,
"ExperimentalEnableAuthenticationTransfer": true,
"CloseUnusedDirectMessages": false,
"EnablePreviewFeatures": true,
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
"ExperimentalGroupUnreadChannels": "disabled",
"ExperimentalChannelOrganization": false,
"ExperimentalChannelSidebarOrganization": "disabled",
"EnableAPIChannelDeletion": true,
"EnableAPITeamDeletion": true,
"ExperimentalEnableHardenedMode": false,
"DisableLegacyMFA": true,
"ExperimentalStrictCSRFEnforcement": false,
"EnableEmailInvitations": true,
"DisableBotsWhenOwnerIsDeactivated": true,
"EnableBotAccountCreation": true,
"EnableSVGs": true,
"EnableLatex": true,
"EnableInlineLatex": true,
"CollapsedThreads": "always_on"
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 2000,
"EnableTeamCreation": true,
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
"EnableCustomUserStatuses": true,
"RestrictCreationToDomains": "",
"EnableCustomBrand": false,
"CustomBrandText": "",
"CustomDescriptionText": "",
"RestrictDirectMessage": "any",
"RestrictTeamInvite": "all",
"RestrictPublicChannelManagement": "all",
"RestrictPrivateChannelManagement": "all",
"RestrictPublicChannelCreation": "all",
"RestrictPrivateChannelCreation": "all",
"RestrictPublicChannelDeletion": "all",
"RestrictPrivateChannelDeletion": "all",
"RestrictPrivateChannelManageMembers": "all",
"EnableXToLeaveChannelsFromLHS": false,
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
"EnableConfirmNotificationsToChannel": true,
"TeammateNameDisplay": "username",
"ExperimentalViewArchivedChannels": true,
"ExperimentalEnableAutomaticReplies": true,
"ExperimentalHideTownSquareinLHS": false,
"ExperimentalTownSquareIsReadOnly": false,
"LockTeammateNameDisplay": false,
"ExperimentalPrimaryTeam": "",
"ExperimentalDefaultChannels": []
},
"ClientRequirements": {
"AndroidLatestVersion": "",
"AndroidMinVersion": "",
"DesktopLatestVersion": "",
"DesktopMinVersion": "",
"IosLatestVersion": "",
"IosMinVersion": ""
},
"LogSettings": {
"EnableConsole": true
},
"NotificationLogSettings": {
"EnableConsole": true
},
"PasswordSettings": {
"MinimumLength": 5,
"Lowercase": false,
"Number": false,
"Uppercase": false,
"Symbol": false,
"Enable": false
},
"FileSettings": {
"EnableFileAttachments": true,
"EnableMobileUpload": true,
"EnableMobileDownload": true,
"MaxFileSize": 52428800,
"DriverName": "local",
"Directory": "./data/",
"EnablePublicLink": false,
"PublicLinkSalt": ""
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
"EnableSignInWithEmail": true,
"EnableSignInWithUsername": true,
"SendEmailNotifications": true,
"UseChannelInEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "test@example.com",
"ReplyToAddress": "test@example.com",
"FeedbackOrganization": "",
"EnableSMTPAuth": false,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "localhost",
"SMTPPort": "10025",
"SMTPServerTimeout": 10,
"ConnectionSecurity": "",
"SendPushNotifications": true,
"PushNotificationServer": "https://push-test.mattermost.com",
"PushNotificationContents": "generic",
"EnableEmailBatching": false,
"EmailBatchingBufferSize": 256,
"EmailBatchingInterval": 30,
"EnablePreviewModeBanner": true,
"SkipServerCertificateVerification": false,
"EmailNotificationContentsType": "full",
"LoginButtonColor": "#0000",
"LoginButtonBorderColor": "#2389D7",
"LoginButtonTextColor": "#2389D7"
},
"RateLimitSettings": {
"Enable": false
},
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true
},
"SupportSettings": {
"TermsOfServiceLink": "https://mattermost.com/terms-of-use/",
"PrivacyPolicyLink": "https://mattermost.com/privacy-policy/",
"AboutLink": "https://mattermost.com/default-about/",
"HelpLink": "https://mattermost.com/default-help/",
"ReportAProblemLink": "https://mattermost.com/default-report-a-problem/",
"SupportEmail": "feedback@mattermost.com",
"CustomTermsOfServiceEnabled": false,
"CustomTermsOfServiceReAcceptancePeriod": 365,
"EnableAskCommunityLink": true
},
"AnnouncementSettings": {
"EnableBanner": false
},
"ThemeSettings": {
"EnableThemeSelection": true,
"DefaultTheme": "default",
"AllowCustomThemes": true,
"AllowedThemes": []
},
"GitLabSettings": {
"Enable": false
},
"GoogleSettings": {
"Enable": false
},
"Office365Settings": {
"Enable": false
},
"LdapSettings": {
"Enable": false
},
"ComplianceSettings": {
"Enable": false
},
"LocalizationSettings": {
"DefaultServerLocale": "en",
"DefaultClientLocale": "en",
"AvailableLocales": ""
},
"SamlSettings": {
"Enable": false
},
"NativeAppSettings": {
"AppDownloadLink": "https://mattermost.com/download/#mattermostApps",
"AndroidAppDownloadLink": "https://mattermost.com/mattermost-android-app/",
"IosAppDownloadLink": "https://mattermost.com/mattermost-ios-app/"
},
"ClusterSettings": {
"Enable": false
},
"MetricsSettings": {
"Enable": false
},
"ExperimentalSettings": {
"ClientSideCertEnable": false,
"ClientSideCertCheck": "secondary",
"LinkMetadataTimeoutMilliseconds": 5000,
"RestrictSystemAdmin": false,
"UseNewSAMLLibrary": false
},
"AnalyticsSettings": {
"MaxUsersForStatistics": 2500
},
"ElasticsearchSettings": {
"ConnectionUrl": "http://localhost:9200",
"Username": "elastic",
"Password": "changeme",
"EnableIndexing": false,
"EnableSearching": false,
"EnableAutocomplete": false,
"Sniff": true
},
"DataRetentionSettings": {
"EnableMessageDeletion": false,
"EnableFileDeletion": false,
"MessageRetentionDays": 365,
"FileRetentionDays": 365,
"DeletionJobStartTime": "02:00"
},
"MessageExportSettings": {
"EnableExport": false
},
"JobSettings": {
"RunJobs": true,
"RunScheduler": true
},
"PluginSettings": {
"Enable": false,
"PluginStates": {
"com.mattermost.calls": {
"Enable": true
}
}
},
"DisplaySettings": {
"CustomUrlSchemes": [],
"ExperimentalTimezone": true
},
"GuestAccountsSettings": {
"Enable": true,
"AllowEmailAccounts": true,
"EnforceMultifactorAuthentication": false,
"RestrictCreationToDomains": ""
},
"ImageProxySettings": {
"Enable": true,
"ImageProxyType": "local",
"RemoteImageProxyURL": "",
"RemoteImageProxyOptions": ""
}
}

View File

@@ -3,13 +3,10 @@
import path from 'path';
import {ldapPort, ldapServer} from '@support/test_config';
import merge from 'deepmerge';
import jestExpect from 'expect';
import client from './client';
import {apiUploadFile, getResponseFromError} from './common';
import defaultServerConfig from './default_config.json';
// ****************************************************************
// System
@@ -152,29 +149,6 @@ export const apiRequireSMTPServer = async (baseUrl: string) => {
jestExpect(status).toEqual(200);
};
/**
* Update configuration.
* See https://api.mattermost.com/#operation/UpdateConfig
* @param {string} baseUrl - the base server URL
* @param {Object} newConfig - specific config to update
* @return {Object} returns {config} on success or {error, status} on error
*/
export const apiUpdateConfig = async (baseUrl: string, newConfig: any = {}): Promise<any> => {
try {
const {config: currentConfig} = await apiGetConfig(baseUrl);
const config = merge.all([currentConfig, getDefaultConfig(baseUrl), newConfig]);
const response = await client.put(
`${baseUrl}/api/v4/config`,
config,
);
return {config: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Upload server license with file expected at "/detox/e2e/support/fixtures/mattermost-license.txt"
* See https://api.mattermost.com/#operation/UploadLicenseFile
@@ -209,18 +183,6 @@ export const getClientLicense = async (baseUrl: string): Promise<any> => {
return {license: out.license};
};
export const getDefaultConfig = (siteUrl: string) => {
const fromEnv = {
LdapSettings: {
LdapServer: ldapServer,
LdapPort: ldapPort,
},
ServiceSettings: {SiteURL: siteUrl},
};
return merge(defaultServerConfig, fromEnv);
};
export const System = {
apiCheckSystemHealth,
apiEmailTest,
@@ -230,10 +192,8 @@ export const System = {
apiRequireLicense,
apiRequireLicenseForFeature,
apiRequireSMTPServer,
apiUpdateConfig,
apiUploadLicense,
getClientLicense,
getDefaultConfig,
};
export default System;

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {isAndroid, timeouts, wait} from '@support/utils';
import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
class ServerScreen {
@@ -47,7 +47,10 @@ class ServerScreen {
await this.serverUrlInput.replaceText(serverUrl);
await this.serverUrlInput.tapReturnKey();
await this.serverDisplayNameInput.replaceText(serverDisplayName);
await this.tapConnectButton();
await this.serverDisplayNameInput.tapReturnKey();
if (isIos()) {
await this.tapConnectButton();
}
};
close = async () => {
@@ -56,10 +59,6 @@ class ServerScreen {
};
tapConnectButton = async () => {
if (isAndroid()) {
await device.pressBack();
await wait(timeouts.ONE_SEC);
}
await this.connectButton.tap();
await wait(timeouts.ONE_SEC);
};

View File

@@ -8,7 +8,6 @@ beforeAll(async () => {
// Login as sysadmin and reset server configuration
await System.apiCheckSystemHealth(siteOneUrl);
await User.apiAdminLogin(siteOneUrl);
await System.apiUpdateConfig(siteOneUrl);
await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl);
await device.launchApp({

View File

@@ -79,7 +79,7 @@ describe('Teams - Invite', () => {
await expect(Invite.teamIcon).toBeVisible();
// * Verify default Selection
await expect(Invite.screenSelection).toBeVisible();
await waitFor(Invite.screenSelection).toBeVisible().withTimeout(timeouts.TWO_SEC);
// * Verify Server data
await expect(Invite.serverDisplayName).toHaveText(serverOneDisplayName);
@@ -93,10 +93,9 @@ describe('Teams - Invite', () => {
});
it('MM-T5221 - should be able to share a URL invite to the team', async () => {
// # Tap on Share link
await Invite.shareLinkButton.tap();
if (isIos()) {
// # Tap on Share link
await Invite.shareLinkButton.tap();
const dialog = systemDialog(`Join the ${testTeam.display_name} team`);
// * Verify share dialog is open
@@ -104,28 +103,28 @@ describe('Teams - Invite', () => {
// # Close share dialog
await dialog.swipe('down');
}
} // no support for Android system dialogs by detox yet. See https://github.com/wix/Detox/issues/3227
});
it('MM-T5361 - should show no results item in search list', async () => {
const noUser = 'qwertyuiop';
// # Search for a non existent user
// # Search for a non-existent user
await Invite.searchBarInput.replaceText(noUser);
// * Validate no results item in search list
await expect(Invite.getSearchListNoResults(noUser)).toBeVisible();
await waitFor(Invite.getSearchListNoResults(noUser)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(Invite.getSearchListNoResultsText(noUser)).toHaveText(noUser);
});
it('MM-T5362 - should be able to send email invite', async () => {
const noUserEmailFormat = 'qwerty@ui.op';
// # Search for a non existent user with email format
// # Search for a non-existent user with email format
await Invite.searchBarInput.replaceText(noUserEmailFormat);
// * Validate email invite item in search list
await expect(Invite.getSearchListTextItem(noUserEmailFormat)).toBeVisible();
await waitFor(Invite.getSearchListTextItem(noUserEmailFormat)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(Invite.getSearchListTextItemText(noUserEmailFormat)).toHaveText(noUserEmailFormat);
// # Select email invite item
@@ -139,7 +138,7 @@ describe('Teams - Invite', () => {
await Invite.sendButton.tap();
// * Validate summary report sent
await expect(Invite.screenSummary).toBeVisible();
await waitFor(Invite.screenSummary).toBeVisible().withTimeout(timeouts.TEN_SEC);
await expect(Invite.getSummaryReportSent()).toBeVisible();
await expect(Invite.getSummaryReportNotSent()).not.toExist();
await expect(Invite.getSummaryReportTextItem(noUserEmailFormat)).toBeVisible();
@@ -149,11 +148,11 @@ describe('Teams - Invite', () => {
it('MM-T5363 - should be able to send user invite', async () => {
const username = ` @${testUser1.username}`;
// # Search for a existent user
// # Search for an existent user
await Invite.searchBarInput.replaceText(testUser1.username);
// * Validate user item in search list
await expect(Invite.getSearchListUserItem(testUser1.id)).toBeVisible();
await waitFor(Invite.getSearchListUserItem(testUser1.id)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(Invite.getSearchListUserItemText(testUser1.id)).toHaveText(username);
// # Select user item
@@ -167,7 +166,7 @@ describe('Teams - Invite', () => {
await Invite.sendButton.tap();
// * Validate summary report sent
await expect(Invite.screenSummary).toBeVisible();
await waitFor(Invite.screenSummary).toBeVisible().withTimeout(timeouts.TEN_SEC);
await expect(Invite.getSummaryReportSent()).toBeVisible();
await expect(Invite.getSummaryReportNotSent()).not.toExist();
await expect(Invite.getSummaryReportUserItem(testUser1.id)).toBeVisible();
@@ -177,11 +176,11 @@ describe('Teams - Invite', () => {
it('MM-T5364 - should not be able to send user invite to user already in team', async () => {
const username = ` @${testUser1.username}`;
// # Search for a existent user already in team
// # Search for an existent user already in team
await Invite.searchBarInput.replaceText(testUser1.username);
// * Validate user item in search list
await expect(Invite.getSearchListUserItem(testUser1.id)).toBeVisible();
await waitFor(Invite.getSearchListUserItem(testUser1.id)).toBeVisible().withTimeout(timeouts.TWO_SEC);
// # Select user item
await Invite.getSearchListUserItem(testUser1.id).tap();
@@ -206,11 +205,11 @@ describe('Teams - Invite', () => {
const username1 = ` @${testUser1.username}`;
const username2 = ` @${testUser2.username}`;
// # Search for a existent user
// # Search for an existent user
await Invite.searchBarInput.replaceText(testUser2.username);
// * Validate user item in search list
await expect(Invite.getSearchListUserItem(testUser2.id)).toBeVisible();
await waitFor(Invite.getSearchListUserItem(testUser2.id)).toBeVisible().withTimeout(timeouts.TEN_SEC);
// # Select user item
await Invite.getSearchListUserItem(testUser2.id).tap();
@@ -234,7 +233,7 @@ describe('Teams - Invite', () => {
await Invite.sendButton.tap();
// * Validate summary
await expect(Invite.screenSummary).toBeVisible();
await waitFor(Invite.screenSummary).toBeVisible().withTimeout(timeouts.TEN_SEC);
// * Validate summary report not sent
await expect(Invite.getSummaryReportNotSent()).toBeVisible();
@@ -242,7 +241,7 @@ describe('Teams - Invite', () => {
await expect(Invite.getSummaryReportUserItemText(testUser1.id)).toHaveText(username1);
// * Validate summary report sent
await expect(Invite.getSummaryReportSent()).toBeVisible();
await waitFor(Invite.getSummaryReportSent()).toBeVisible().withTimeout(timeouts.TEN_SEC);
await expect(Invite.getSummaryReportUserItem(testUser2.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser2.id)).toHaveText(username2);
});

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 = 459;
CURRENT_PROJECT_VERSION = 460;
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 = 459;
CURRENT_PROJECT_VERSION = 460;
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 = 459;
CURRENT_PROJECT_VERSION = 460;
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 = 459;
CURRENT_PROJECT_VERSION = 460;
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>459</string>
<string>460</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>459</string>
<string>460</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>459</string>
<string>460</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

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|@mmcalls/common)',
],
};

39
package-lock.json generated
View File

@@ -19,10 +19,11 @@
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.2",
"@mattermost/react-native-network-client": "1.3.1",
"@mattermost/react-native-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4",
"@mmcalls/common": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"@msgpack/msgpack": "2.8.0",
"@nozbe/watermelondb": "0.25.5",
"@nozbe/with-observables": "1.4.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",
@@ -3241,9 +3243,9 @@
}
},
"node_modules/@mattermost/react-native-network-client": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
"integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz",
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==",
"dependencies": {
"validator": "13.9.0",
"zod": "3.20.6"
@@ -3286,6 +3288,13 @@
"react-native": "*"
}
},
"node_modules/@mmcalls/common": {
"name": "@calls/common",
"version": "0.12.0",
"resolved": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"integrity": "sha512-lz+T2HHQBfQDpgrI4CkrSNkmBjIaKxJGSxSZHSL0LQ8xAlRUBo3WMG5wJlwwzpxP6rixJjUnoyKBSo8Jn0wWQQ==",
"license": "MIT"
},
"node_modules/@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
@@ -5957,6 +5966,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",
@@ -24297,9 +24312,9 @@
"requires": {}
},
"@mattermost/react-native-network-client": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
"integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz",
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==",
"requires": {
"validator": "13.9.0",
"zod": "3.20.6"
@@ -24325,6 +24340,10 @@
"integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==",
"requires": {}
},
"@mmcalls/common": {
"version": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"integrity": "sha512-lz+T2HHQBfQDpgrI4CkrSNkmBjIaKxJGSxSZHSL0LQ8xAlRUBo3WMG5wJlwwzpxP6rixJjUnoyKBSo8Jn0wWQQ=="
},
"@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
@@ -26328,6 +26347,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",

View File

@@ -16,10 +16,11 @@
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.2",
"@mattermost/react-native-network-client": "1.3.1",
"@mattermost/react-native-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4",
"@mmcalls/common": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"@msgpack/msgpack": "2.8.0",
"@nozbe/watermelondb": "0.25.5",
"@nozbe/with-observables": "1.4.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",