forked from Ivasoft/mattermost-mobile
Compare commits
1 Commits
test1.0.5
...
MM-50010-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bcea5a6a3 |
@@ -43,14 +43,13 @@ import type {
|
|||||||
ApiResp,
|
ApiResp,
|
||||||
Call,
|
Call,
|
||||||
CallParticipant,
|
CallParticipant,
|
||||||
CallReactionEmoji,
|
|
||||||
CallsConnection,
|
CallsConnection,
|
||||||
RecordingState,
|
|
||||||
ServerCallState,
|
ServerCallState,
|
||||||
ServerChannelState,
|
ServerChannelState,
|
||||||
} from '@calls/types/calls';
|
} from '@calls/types/calls';
|
||||||
import type {Client} from '@client/rest';
|
import type {Client} from '@client/rest';
|
||||||
import type ClientError from '@client/rest/error';
|
import type ClientError from '@client/rest/error';
|
||||||
|
import type {CallRecordingState, EmojiData} from '@mmcalls/common/lib/types';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
|
||||||
let connection: CallsConnection | null = null;
|
let connection: CallsConnection | null = null;
|
||||||
@@ -322,7 +321,7 @@ export const unraiseHand = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendReaction = (emoji: CallReactionEmoji) => {
|
export const sendReaction = (emoji: EmojiData) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.sendReaction(emoji);
|
connection.sendReaction(emoji);
|
||||||
}
|
}
|
||||||
@@ -415,7 +414,7 @@ export const startCallRecording = async (serverUrl: string, callId: string) => {
|
|||||||
|
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
|
||||||
let data: ApiResp | RecordingState;
|
let data: ApiResp | CallRecordingState;
|
||||||
try {
|
try {
|
||||||
data = await client.startCallRecording(callId);
|
data = await client.startCallRecording(callId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -433,7 +432,7 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => {
|
|||||||
|
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
|
||||||
let data: ApiResp | RecordingState;
|
let data: ApiResp | CallRecordingState;
|
||||||
try {
|
try {
|
||||||
data = await client.stopCallRecording(callId);
|
data = await client.stopCallRecording(callId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {
|
import type {ServerChannelState, ApiResp} from '@calls/types/calls';
|
||||||
ServerChannelState,
|
import type {CallRecordingState, CallsConfig} from '@mmcalls/common/lib/types';
|
||||||
ServerCallsConfig,
|
|
||||||
ApiResp,
|
|
||||||
RecordingState,
|
|
||||||
} from '@calls/types/calls';
|
|
||||||
import type {RTCIceServer} from 'react-native-webrtc';
|
import type {RTCIceServer} from 'react-native-webrtc';
|
||||||
|
|
||||||
export interface ClientCallsMix {
|
export interface ClientCallsMix {
|
||||||
getEnabled: () => Promise<Boolean>;
|
getEnabled: () => Promise<Boolean>;
|
||||||
getCalls: () => Promise<ServerChannelState[]>;
|
getCalls: () => Promise<ServerChannelState[]>;
|
||||||
getCallForChannel: (channelId: string) => Promise<ServerChannelState>;
|
getCallForChannel: (channelId: string) => Promise<ServerChannelState>;
|
||||||
getCallsConfig: () => Promise<ServerCallsConfig>;
|
getCallsConfig: () => Promise<CallsConfig>;
|
||||||
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
|
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
|
||||||
endCall: (channelId: string) => Promise<ApiResp>;
|
endCall: (channelId: string) => Promise<ApiResp>;
|
||||||
genTURNCredentials: () => Promise<RTCIceServer[]>;
|
genTURNCredentials: () => Promise<RTCIceServer[]>;
|
||||||
startCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
|
startCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
|
||||||
stopCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
|
stopCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientCalls = (superclass: any) => class extends superclass {
|
const ClientCalls = (superclass: any) => class extends superclass {
|
||||||
@@ -52,7 +48,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
|||||||
return this.doFetch(
|
return this.doFetch(
|
||||||
`${this.getCallsRoute()}/config`,
|
`${this.getCallsRoute()}/config`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
) as ServerCallsConfig;
|
) as CallsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
enableChannelCalls = async (channelId: string, enable: boolean) => {
|
enableChannelCalls = async (channelId: string, enable: boolean) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import CompassIcon from '@components/compass_icon';
|
|||||||
import Emoji from '@components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import ProfilePicture from '@components/profile_picture';
|
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';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,7 +18,7 @@ type Props = {
|
|||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
sharingScreen?: boolean;
|
sharingScreen?: boolean;
|
||||||
raisedHand?: boolean;
|
raisedHand?: boolean;
|
||||||
reaction?: CallReactionEmoji;
|
reaction?: EmojiData;
|
||||||
size?: 'm' | 'l';
|
size?: 'm' | 'l';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
import {RTCPeer} from '@mmcalls/common/lib';
|
||||||
// @ts-ignore
|
import {deflate} from 'pako';
|
||||||
import {deflate} from 'pako/lib/deflate.js';
|
|
||||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native';
|
import {DeviceEventEmitter, EmitterSubscription} from 'react-native';
|
||||||
import InCallManager from 'react-native-incall-manager';
|
import InCallManager from 'react-native-incall-manager';
|
||||||
import {
|
import {
|
||||||
MediaStream,
|
MediaStream,
|
||||||
MediaStreamTrack,
|
MediaStreamTrack,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
|
RTCPeerConnection,
|
||||||
} from 'react-native-webrtc';
|
} from 'react-native-webrtc';
|
||||||
|
|
||||||
import RTCPeer from '@calls/rtcpeer';
|
|
||||||
import {setSpeakerPhone} from '@calls/state';
|
import {setSpeakerPhone} from '@calls/state';
|
||||||
import {getICEServersConfigs} from '@calls/utils';
|
import {getICEServersConfigs} from '@calls/utils';
|
||||||
import {WebsocketEvents} from '@constants';
|
import {WebsocketEvents} from '@constants';
|
||||||
@@ -22,7 +21,9 @@ import {logError, logDebug, logWarning} from '@utils/log';
|
|||||||
|
|
||||||
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
|
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;
|
const peerConnectTimeout = 5000;
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ export async function newConnection(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendReaction = (emoji: CallReactionEmoji) => {
|
const sendReaction = (emoji: EmojiData) => {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.send('react', {
|
ws.send('react', {
|
||||||
data: JSON.stringify(emoji),
|
data: JSON.stringify(emoji),
|
||||||
@@ -204,7 +205,14 @@ export async function newConnection(
|
|||||||
InCallManager.start({media: 'video'});
|
InCallManager.start({media: 'video'});
|
||||||
setSpeakerPhone(true);
|
setSpeakerPhone(true);
|
||||||
|
|
||||||
peer = new RTCPeer({iceServers: iceConfigs || []});
|
const opts: RTCPeerOpts = {
|
||||||
|
logDebug,
|
||||||
|
webrtc: {
|
||||||
|
MediaStream,
|
||||||
|
RTCPeerConnection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
peer = new RTCPeer({iceServers: iceConfigs || []}, opts);
|
||||||
|
|
||||||
peer.on('offer', (sdp) => {
|
peer.on('offer', (sdp) => {
|
||||||
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);
|
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -40,8 +40,6 @@ import {
|
|||||||
setPluginEnabled,
|
setPluginEnabled,
|
||||||
setUserVoiceOn,
|
setUserVoiceOn,
|
||||||
} from '@calls/state/actions';
|
} from '@calls/state/actions';
|
||||||
import {License} from '@constants';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Call,
|
Call,
|
||||||
CallsState,
|
CallsState,
|
||||||
@@ -51,8 +49,10 @@ import {
|
|||||||
DefaultCurrentCall,
|
DefaultCurrentCall,
|
||||||
DefaultGlobalCallsState,
|
DefaultGlobalCallsState,
|
||||||
GlobalCallsState,
|
GlobalCallsState,
|
||||||
RecordingState,
|
} from '@calls/types/calls';
|
||||||
} from '../types/calls';
|
import {License} from '@constants';
|
||||||
|
|
||||||
|
import type {CallRecordingState} from '@mmcalls/common/lib/types';
|
||||||
|
|
||||||
jest.mock('@calls/alerts');
|
jest.mock('@calls/alerts');
|
||||||
|
|
||||||
@@ -797,6 +797,7 @@ describe('useCallsState', () => {
|
|||||||
|
|
||||||
it('config', () => {
|
it('config', () => {
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
|
...DefaultCallsConfig,
|
||||||
ICEServers: [],
|
ICEServers: [],
|
||||||
ICEServersConfigs: [
|
ICEServersConfigs: [
|
||||||
{
|
{
|
||||||
@@ -914,7 +915,7 @@ describe('useCallsState', () => {
|
|||||||
myUserId: 'myUserId',
|
myUserId: 'myUserId',
|
||||||
...call1,
|
...call1,
|
||||||
};
|
};
|
||||||
const recState: RecordingState = {
|
const recState: CallRecordingState = {
|
||||||
init_at: 123,
|
init_at: 123,
|
||||||
start_at: 231,
|
start_at: 231,
|
||||||
end_at: 345,
|
end_at: 345,
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ import {
|
|||||||
} from '@calls/state';
|
} from '@calls/state';
|
||||||
import {
|
import {
|
||||||
Call,
|
Call,
|
||||||
CallReaction,
|
CallsConfigState,
|
||||||
CallsConfig,
|
|
||||||
ChannelsWithCalls,
|
ChannelsWithCalls,
|
||||||
CurrentCall,
|
CurrentCall,
|
||||||
DefaultCall,
|
DefaultCall,
|
||||||
DefaultCurrentCall,
|
DefaultCurrentCall,
|
||||||
ReactionStreamEmoji,
|
ReactionStreamEmoji,
|
||||||
RecordingState,
|
|
||||||
} from '@calls/types/calls';
|
} from '@calls/types/calls';
|
||||||
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/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>) => {
|
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
|
||||||
const channelsWithCalls = Object.keys(calls).reduce(
|
const channelsWithCalls = Object.keys(calls).reduce(
|
||||||
(accum, next) => {
|
(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);
|
const callsConfig = getCallsConfig(serverUrl);
|
||||||
setCallsConfig(serverUrl, {...callsConfig, ...config});
|
setCallsConfig(serverUrl, {...callsConfig, ...config});
|
||||||
};
|
};
|
||||||
@@ -423,7 +423,7 @@ export const setMicPermissionsErrorDismissed = () => {
|
|||||||
setCurrentCall(nextCurrentCall);
|
setCurrentCall(nextCurrentCall);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => {
|
export const userReacted = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
|
||||||
// Note: Simplification for performance:
|
// Note: Simplification for performance:
|
||||||
// If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your
|
// 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.
|
// 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);
|
}, REACTION_TIMEOUT);
|
||||||
};
|
};
|
||||||
|
|
||||||
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => {
|
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
|
||||||
const currentCall = getCurrentCall();
|
const currentCall = getCurrentCall();
|
||||||
if (currentCall?.channelId !== channelId) {
|
if (currentCall?.channelId !== channelId) {
|
||||||
return;
|
return;
|
||||||
@@ -498,7 +498,7 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Cal
|
|||||||
setCurrentCall(nextCurrentCall);
|
setCurrentCall(nextCurrentCall);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => {
|
export const setRecordingState = (serverUrl: string, channelId: string, recState: CallRecordingState) => {
|
||||||
const callsState = getCallsState(serverUrl);
|
const callsState = getCallsState(serverUrl);
|
||||||
if (!callsState.calls[channelId]) {
|
if (!callsState.calls[channelId]) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {BehaviorSubject} from 'rxjs';
|
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) => {
|
const getCallsConfigSubject = (serverUrl: string) => {
|
||||||
if (!callsConfigSubjects[serverUrl]) {
|
if (!callsConfigSubjects[serverUrl]) {
|
||||||
@@ -20,7 +20,7 @@ export const getCallsConfig = (serverUrl: string) => {
|
|||||||
return getCallsConfigSubject(serverUrl).value;
|
return getCallsConfigSubject(serverUrl).value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfig) => {
|
export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfigState) => {
|
||||||
getCallsConfigSubject(serverUrl).next(callsConfig);
|
getCallsConfigSubject(serverUrl).next(callsConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 UserModel from '@typings/database/models/servers/user';
|
||||||
import type {RTCIceServer} from 'react-native-webrtc';
|
|
||||||
|
|
||||||
export type GlobalCallsState = {
|
export type GlobalCallsState = {
|
||||||
micPermissionsGranted: boolean;
|
micPermissionsGranted: boolean;
|
||||||
@@ -31,7 +31,7 @@ export type Call = {
|
|||||||
screenOn: string;
|
screenOn: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
recState?: RecordingState;
|
recState?: CallRecordingState;
|
||||||
hostId: string;
|
hostId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export type CallParticipant = {
|
|||||||
muted: boolean;
|
muted: boolean;
|
||||||
raisedHand: number;
|
raisedHand: number;
|
||||||
userModel?: UserModel;
|
userModel?: UserModel;
|
||||||
reaction?: CallReaction;
|
reaction?: UserReactionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelsWithCalls = Dictionary<boolean>;
|
export type ChannelsWithCalls = Dictionary<boolean>;
|
||||||
@@ -100,7 +100,7 @@ export type ServerCallState = {
|
|||||||
screen_sharing_id: string;
|
screen_sharing_id: string;
|
||||||
owner_id: string;
|
owner_id: string;
|
||||||
host_id: string;
|
host_id: string;
|
||||||
recording: RecordingState;
|
recording: CallRecordingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CallsConnection = {
|
export type CallsConnection = {
|
||||||
@@ -111,26 +111,16 @@ export type CallsConnection = {
|
|||||||
raiseHand: () => void;
|
raiseHand: () => void;
|
||||||
unraiseHand: () => void;
|
unraiseHand: () => void;
|
||||||
initializeVoiceTrack: () => void;
|
initializeVoiceTrack: () => void;
|
||||||
sendReaction: (emoji: CallReactionEmoji) => void;
|
sendReaction: (emoji: EmojiData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerCallsConfig = {
|
export type CallsConfigState = CallsConfig & {
|
||||||
ICEServers?: string[]; // deprecated
|
|
||||||
ICEServersConfigs: RTCIceServer[];
|
|
||||||
AllowEnableCalls: boolean;
|
AllowEnableCalls: boolean;
|
||||||
DefaultEnabled: boolean;
|
|
||||||
NeedsTURNCredentials: boolean;
|
|
||||||
sku_short_name: string;
|
|
||||||
MaxCallParticipants: number;
|
|
||||||
EnableRecordings: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CallsConfig = ServerCallsConfig & {
|
|
||||||
pluginEnabled: boolean;
|
pluginEnabled: boolean;
|
||||||
last_retrieved_at: number;
|
last_retrieved_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultCallsConfig: CallsConfig = {
|
export const DefaultCallsConfig: CallsConfigState = {
|
||||||
pluginEnabled: false,
|
pluginEnabled: false,
|
||||||
ICEServers: [], // deprecated
|
ICEServers: [], // deprecated
|
||||||
ICEServersConfigs: [],
|
ICEServersConfigs: [],
|
||||||
@@ -141,6 +131,8 @@ export const DefaultCallsConfig: CallsConfig = {
|
|||||||
sku_short_name: '',
|
sku_short_name: '',
|
||||||
MaxCallParticipants: 0,
|
MaxCallParticipants: 0,
|
||||||
EnableRecordings: false,
|
EnableRecordings: false,
|
||||||
|
MaxRecordingDuration: 60,
|
||||||
|
AllowScreenSharing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiResp = {
|
export type ApiResp = {
|
||||||
@@ -149,18 +141,6 @@ export type ApiResp = {
|
|||||||
status_code: number;
|
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 = {
|
export type ReactionStreamEmoji = {
|
||||||
name: string;
|
name: string;
|
||||||
latestTimestamp: number;
|
latestTimestamp: number;
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
|
||||||
import {License} from '@constants';
|
import {License} from '@constants';
|
||||||
|
|
||||||
import {getICEServersConfigs} from './utils';
|
import {getICEServersConfigs} from './utils';
|
||||||
|
|
||||||
import type {CallsConfig} from '@calls/types/calls';
|
|
||||||
|
|
||||||
describe('getICEServersConfigs', () => {
|
describe('getICEServersConfigs', () => {
|
||||||
it('backwards compatible case, no ICEServersConfigs present', () => {
|
it('backwards compatible case, no ICEServersConfigs present', () => {
|
||||||
const config: CallsConfig = {
|
const config: CallsConfigState = {
|
||||||
|
...DefaultCallsConfig,
|
||||||
pluginEnabled: true,
|
pluginEnabled: true,
|
||||||
ICEServers: ['stun:stun.example.com:3478'],
|
ICEServers: ['stun:stun.example.com:3478'],
|
||||||
ICEServersConfigs: [],
|
ICEServersConfigs: [],
|
||||||
@@ -33,7 +33,8 @@ describe('getICEServersConfigs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ICEServersConfigs set', () => {
|
it('ICEServersConfigs set', () => {
|
||||||
const config: CallsConfig = {
|
const config: CallsConfigState = {
|
||||||
|
...DefaultCallsConfig,
|
||||||
pluginEnabled: true,
|
pluginEnabled: true,
|
||||||
ICEServersConfigs: [
|
ICEServersConfigs: [
|
||||||
{
|
{
|
||||||
@@ -64,7 +65,8 @@ describe('getICEServersConfigs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Both ICEServers and ICEServersConfigs set', () => {
|
it('Both ICEServers and ICEServersConfigs set', () => {
|
||||||
const config: CallsConfig = {
|
const config: CallsConfigState = {
|
||||||
|
...DefaultCallsConfig,
|
||||||
pluginEnabled: true,
|
pluginEnabled: true,
|
||||||
ICEServers: ['stun:stuna.example.com:3478'],
|
ICEServers: ['stun:stuna.example.com:3478'],
|
||||||
ICEServersConfigs: [
|
ICEServersConfigs: [
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import Calls from '@constants/calls';
|
|||||||
import {isMinimumServerVersion} from '@utils/helpers';
|
import {isMinimumServerVersion} from '@utils/helpers';
|
||||||
import {displayUsername} from '@utils/user';
|
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 PostModel from '@typings/database/models/servers/post';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
import type {RTCIceServer} from 'react-native-webrtc';
|
||||||
|
|
||||||
export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
|
export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
|
||||||
if (!participants) {
|
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
|
// if ICEServersConfigs is set, we can trust this to be complete and
|
||||||
// coming from an updated API.
|
// coming from an updated API.
|
||||||
if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) {
|
if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) {
|
||||||
|
|||||||
@@ -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',
|
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/file_transformer.js',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
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)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"@mattermost/react-native-paste-input": "0.6.2",
|
"@mattermost/react-native-paste-input": "0.6.2",
|
||||||
"@mattermost/react-native-turbo-log": "0.2.3",
|
"@mattermost/react-native-turbo-log": "0.2.3",
|
||||||
"@mattermost/react-native-turbo-mailer": "0.2.4",
|
"@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",
|
"@msgpack/msgpack": "2.8.0",
|
||||||
"@nozbe/watermelondb": "0.25.5",
|
"@nozbe/watermelondb": "0.25.5",
|
||||||
"@nozbe/with-observables": "1.4.1",
|
"@nozbe/with-observables": "1.4.1",
|
||||||
@@ -124,6 +125,7 @@
|
|||||||
"@types/jest": "29.4.0",
|
"@types/jest": "29.4.0",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/mime-db": "1.43.1",
|
"@types/mime-db": "1.43.1",
|
||||||
|
"@types/pako": "2.0.0",
|
||||||
"@types/querystringify": "2.0.0",
|
"@types/querystringify": "2.0.0",
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-native-background-timer": "2.0.0",
|
"@types/react-native-background-timer": "2.0.0",
|
||||||
@@ -3286,6 +3288,13 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/@msgpack/msgpack": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
|
"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": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
@@ -24325,6 +24340,10 @@
|
|||||||
"integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==",
|
"integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"@mmcalls/common": {
|
||||||
|
"version": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
|
||||||
|
"integrity": "sha512-lz+T2HHQBfQDpgrI4CkrSNkmBjIaKxJGSxSZHSL0LQ8xAlRUBo3WMG5wJlwwzpxP6rixJjUnoyKBSo8Jn0wWQQ=="
|
||||||
|
},
|
||||||
"@msgpack/msgpack": {
|
"@msgpack/msgpack": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
|
"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": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@mattermost/react-native-paste-input": "0.6.2",
|
"@mattermost/react-native-paste-input": "0.6.2",
|
||||||
"@mattermost/react-native-turbo-log": "0.2.3",
|
"@mattermost/react-native-turbo-log": "0.2.3",
|
||||||
"@mattermost/react-native-turbo-mailer": "0.2.4",
|
"@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",
|
"@msgpack/msgpack": "2.8.0",
|
||||||
"@nozbe/watermelondb": "0.25.5",
|
"@nozbe/watermelondb": "0.25.5",
|
||||||
"@nozbe/with-observables": "1.4.1",
|
"@nozbe/with-observables": "1.4.1",
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
"@types/jest": "29.4.0",
|
"@types/jest": "29.4.0",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/mime-db": "1.43.1",
|
"@types/mime-db": "1.43.1",
|
||||||
|
"@types/pako": "2.0.0",
|
||||||
"@types/querystringify": "2.0.0",
|
"@types/querystringify": "2.0.0",
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-native-background-timer": "2.0.0",
|
"@types/react-native-background-timer": "2.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user