Voicechannels (#5753)
* Some extra work on voice channels interface * Fixing some TODOs * Improving styling of call in channel * Improve calls monitoring * Replacing some of the fontawesome icons with the compass ones * Improving the layout * Migrating to webrtc2 for unified plan * Add screen on and off behavior * Adding incall manager plugin * Moving everything into the products/calls folder * Make products modules routes relatives * Make products modules routes @mmproducts * Removing initiator parameter * Removing trickle parameter * Simplifying code * Removing underscore from private variables * Removing underscore from private things * More simplifications * More simplifications * More simplifications * Changing sha sum for mmjstool * Fixing typo * Migrating simple-peer to typescript * Migrating simple-peer to typescript * Improving the size of the screen share * Adding feature flag to disable the calls feature in mobile * Fixing some tests * Removing obsolte tests * Added call ended support for the post messages * Fixing some warnings in the tests * Adding JoinCall tests * Adding CallMessage tests * Adding CurrentCall unit tests * Adding CallAvatar unit tests * Adding FloatingCallContainer unit tests * Adding StartCall unit tests * Adding EnableDisableCalls unit tests * Adding CallDuration tests * Improving CallDuration tests * Adding CallScreen unit tests * Adding CallOtherActions screen tests * Fixing some dark theme styles * Fixing tests * More robustness around connecting/disconnecting * Adding FormattedRelativeTime tests * Adding tests for ChannelItem * Adding tests for ChannelInfo * Adding selectors tests * Adding reducers unit tests * Adding actions tests * Removing most of the TODOs * Removing another TODO * Updating tests snapshots * Removing the last TODO * Fixed a small problem on pressing while a call is ongoing * Remove all the inlined functions * Replacing usage of isLandscape selector with useWindowDimensions * Removed unnecesary makeStyleSheetFromTheme * Removing unneded properties from call_duration * Fixing possible null channels return from getChannel selector * Moving other inlined functions to its own constant * Simplifiying enable/disable calls component * Improving the behavior when you are in the call of the current channel * Adding missing translation strings * Simplified a bit the EnableDisableCalls component * Moving other inlined functions to its own constant * Updating snapshots * Improving usage of makeStyleSheetFromTheme * Moving data reformating from the rest client to the redux action * Adding calls to the blocklist to the redux-persist * Fixing tests * Updating snapshots * Update file icon name to the last compass icons version * Fix loading state * Only show the call connected if the websocket gets connected * Taking into consideration the indicator bar to position the calls new bars * Making the MoreMessagesButton component aware of calls components * Updating snapshots * Fixing tests * Updating snapshot * Fixing different use cases for start call channel menu * Fixing tests * Ask for confirmation to start a call when you are already in another call * Update app/products/calls/components/floating_call_container.tsx Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * Memoizing userIds in join call * Applying suggestion around combine the blocklist for calls with the one for typing * Adding explicit types to the rest client * Removing unneeded permission * Making updateIntervalInSeconds prop optional in FormattedRelativeTime * Making updateIntervalInSeconds prop optional in CallDuration Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
eb8579e84c
commit
49462bd4a0
103 changed files with 17461 additions and 3082 deletions
|
|
@ -9,6 +9,10 @@
|
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -11,7 +11,7 @@ import {getThreads} from '@mm-redux/actions/threads';
|
|||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
|
@ -23,6 +23,20 @@ import {TeamMembership} from '@mm-redux/types/teams';
|
|||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {loadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {
|
||||
handleCallStarted,
|
||||
handleCallUserConnected,
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserVoiceOn,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallChannelEnabled,
|
||||
handleCallChannelDisabled,
|
||||
handleCallScreenOn,
|
||||
handleCallScreenOff,
|
||||
} from '@mmproducts/calls/store/actions/websockets';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import websocketClient from '@websocket';
|
||||
|
||||
|
|
@ -147,6 +161,10 @@ export function doReconnect(now: number) {
|
|||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
|
||||
if (!me.error) {
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
dispatch(loadCalls());
|
||||
}
|
||||
|
||||
const roles = [];
|
||||
|
||||
if (me.roles?.length) {
|
||||
|
|
@ -325,7 +343,7 @@ function handleClose(connectFailCount: number) {
|
|||
}
|
||||
|
||||
function handleEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
|
|
@ -421,6 +439,33 @@ function handleEvent(msg: WebSocketMessage) {
|
|||
return dispatch(handleSidebarCategoryOrderUpdated(msg));
|
||||
}
|
||||
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
|
||||
return dispatch(handleCallChannelEnabled(msg));
|
||||
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
|
||||
return dispatch(handleCallChannelDisabled(msg));
|
||||
case WebsocketEvents.CALLS_USER_CONNECTED:
|
||||
return dispatch(handleCallUserConnected(msg));
|
||||
case WebsocketEvents.CALLS_USER_DISCONNECTED:
|
||||
return dispatch(handleCallUserDisconnected(msg));
|
||||
case WebsocketEvents.CALLS_USER_MUTED:
|
||||
return dispatch(handleCallUserMuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_UNMUTED:
|
||||
return dispatch(handleCallUserUnmuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_VOICE_ON:
|
||||
return dispatch(handleCallUserVoiceOn(msg));
|
||||
case WebsocketEvents.CALLS_USER_VOICE_OFF:
|
||||
return dispatch(handleCallUserVoiceOff(msg));
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
return dispatch(handleCallStarted(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
return dispatch(handleCallScreenOn(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
return dispatch(handleCallScreenOff(msg));
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,6 +290,10 @@ export default class ClientBase {
|
|||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.calls`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
|
||||
const serverUrl = this.getUrl();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
|
||||
import mix from '@utils/mix';
|
||||
|
||||
import ClientApps, {ClientAppsMix} from './apps';
|
||||
|
|
@ -34,7 +35,8 @@ interface Client extends ClientBase,
|
|||
ClientSharedChannelsMix,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix
|
||||
ClientUsersMix,
|
||||
ClientCallsMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
|
|
@ -52,6 +54,7 @@ class Client extends mix(ClientBase).with(
|
|||
ClientTeams,
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattedRelativeTime should match snapshot 1`] = `
|
||||
<Text>
|
||||
a few seconds ago
|
||||
</Text>
|
||||
`;
|
||||
|
|
@ -86,6 +86,7 @@ export interface AvatarsProps {
|
|||
breakAt?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
theme: Theme;
|
||||
listTitle?: JSX.Element;
|
||||
}
|
||||
|
||||
export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
|
|
@ -94,11 +95,12 @@ export default class Avatars extends PureComponent<AvatarsProps> {
|
|||
};
|
||||
|
||||
showParticipantsList = () => {
|
||||
const {userIds} = this.props;
|
||||
const {userIds, listTitle} = this.props;
|
||||
|
||||
const screen = 'ParticipantsList';
|
||||
const passProps = {
|
||||
userIds,
|
||||
listTitle,
|
||||
};
|
||||
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
|
|
|
|||
62
app/components/formatted_relative_time.test.js
Normal file
62
app/components/formatted_relative_time.test.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import FormattedRelativeTime from './formatted_relative_time';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: (f) => f(),
|
||||
}));
|
||||
|
||||
describe('FormattedRelativeTime', () => {
|
||||
const baseProps = {
|
||||
value: moment.now() - 15000,
|
||||
updateIntervalInSeconds: 10000,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FormattedRelativeTime {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match string in the past', () => {
|
||||
const props = {...baseProps, value: moment.now() - ((10 * 60 * 60 * 1000) + (30 * 60 * 1000) + (25 * 1000) + 500)};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('11 hours ago');
|
||||
});
|
||||
|
||||
test('should match string in the future', () => {
|
||||
const props = {...baseProps, value: moment.now() + 15500};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('in a few seconds');
|
||||
});
|
||||
|
||||
test('should re-render after updateIntervalInSeconds', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {...baseProps, value: moment.now(), updateIntervalInSeconds: 120};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(60000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000);
|
||||
expect(wrapper.getElement().props.children).toBe('2 minutes ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should not re-render if updateIntervalInSeconds is not passed', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {value: baseProps.value};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000000000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
44
app/components/formatted_relative_time.tsx
Normal file
44
app/components/formatted_relative_time.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Text, TextProps} from 'react-native';
|
||||
|
||||
import type {UserTimezone} from '@mm-redux/types/users';
|
||||
|
||||
type FormattedRelativeTimeProps = TextProps & {
|
||||
timezone?: UserTimezone | string;
|
||||
value: number | string | Date;
|
||||
updateIntervalInSeconds?: number;
|
||||
}
|
||||
|
||||
const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...props}: FormattedRelativeTimeProps) => {
|
||||
const getFormattedRelativeTime = () => {
|
||||
let zone = timezone;
|
||||
if (typeof timezone === 'object') {
|
||||
zone = timezone.useAutomaticTimezone ? timezone.automaticTimezone : timezone.manualTimezone;
|
||||
}
|
||||
|
||||
return timezone ? moment.tz(value, zone as string).fromNow() : moment(value).fromNow();
|
||||
};
|
||||
|
||||
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime());
|
||||
useEffect(() => {
|
||||
if (updateIntervalInSeconds) {
|
||||
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => null;
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
<Text {...props}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormattedRelativeTime;
|
||||
|
|
@ -513,7 +513,7 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
|||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.64)"
|
||||
name="file-document-outline"
|
||||
name="file-text-outline"
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ exports[`FileQuickAction should match snapshot 1`] = `
|
|||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.64)"
|
||||
name="file-document-outline"
|
||||
name="file-text-outline"
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const FileQuickAction = ({disabled, fileCount = 0, intl, maxFileCount, onUploadF
|
|||
>
|
||||
<CompassIcon
|
||||
color={color}
|
||||
name='file-document-outline'
|
||||
name='file-text-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
|
|||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateY": -438,
|
||||
"translateY": -550,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {shallowWithIntl} from '@test/intl-test-helper';
|
|||
import MoreMessagesButton, {
|
||||
MIN_INPUT,
|
||||
MAX_INPUT,
|
||||
INDICATOR_BAR_FACTOR,
|
||||
BARS_FACTOR,
|
||||
CANCEL_TIMER_DELAY,
|
||||
} from './more_messages_button';
|
||||
|
||||
|
|
@ -296,17 +296,19 @@ describe('MoreMessagesButton', () => {
|
|||
expect(Animated.spring).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should animate to MAX_INPUT - INDICATOR_BAR_FACTOR if visible and indicator bar hides', () => {
|
||||
it('should animate to MAX_INPUT - BARS_FACTOR if visible and indicator bar hides', () => {
|
||||
instance.buttonVisible = true;
|
||||
instance.onIndicatorBarVisible(false);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
toValue: MAX_INPUT - BARS_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should animate to MAX_INPUT if visible and indicator becomes visible', () => {
|
||||
instance.buttonVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.onIndicatorBarVisible(true);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT,
|
||||
|
|
@ -414,13 +416,15 @@ describe('MoreMessagesButton', () => {
|
|||
instance.show();
|
||||
expect(instance.buttonVisible).toBe(true);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
toValue: MAX_INPUT - BARS_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should account for the indicator bar height when the indicator is visible', () => {
|
||||
it('should account for the indicator bar heights when the indicator is visible', () => {
|
||||
instance.indicatorBarVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.buttonVisible = false;
|
||||
wrapper.setState({moreText: '1 new message'});
|
||||
wrapper.setProps({deepLinkURL: null, unreadCount: 1});
|
||||
|
|
@ -457,13 +461,15 @@ describe('MoreMessagesButton', () => {
|
|||
instance.hide();
|
||||
expect(instance.buttonVisible).toBe(false);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MIN_INPUT + INDICATOR_BAR_FACTOR,
|
||||
toValue: MIN_INPUT + BARS_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should account for the indicator bar height when the indicator is visible', () => {
|
||||
it('should account for the indicator bars heights when the indicator is visible', () => {
|
||||
instance.indicatorBarVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.buttonVisible = true;
|
||||
|
||||
instance.hide();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {ActivityIndicator, Animated, AppState, AppStateStatus, NativeEventSubscr
|
|||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import ViewTypes, {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import ViewTypes, {INDICATOR_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT, CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {messageCount} from '@mm-redux/utils/post_list';
|
||||
import {t} from '@utils/i18n';
|
||||
|
|
@ -17,22 +17,22 @@ import type {Theme} from '@mm-redux/types/theme';
|
|||
|
||||
const HIDDEN_TOP = -400;
|
||||
const SHOWN_TOP = 0;
|
||||
export const INDICATOR_BAR_FACTOR = Math.abs(INDICATOR_BAR_HEIGHT / (HIDDEN_TOP - SHOWN_TOP));
|
||||
export const BARS_FACTOR = Math.abs((INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
export const MIN_INPUT = 0;
|
||||
export const MAX_INPUT = 1;
|
||||
|
||||
const TOP_INTERPOL_CONFIG: Animated.InterpolationConfigType = {
|
||||
inputRange: [
|
||||
MIN_INPUT,
|
||||
MIN_INPUT + INDICATOR_BAR_FACTOR,
|
||||
MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
MIN_INPUT + BARS_FACTOR,
|
||||
MAX_INPUT - BARS_FACTOR,
|
||||
MAX_INPUT,
|
||||
],
|
||||
outputRange: [
|
||||
HIDDEN_TOP - INDICATOR_BAR_HEIGHT,
|
||||
HIDDEN_TOP - (INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT),
|
||||
HIDDEN_TOP,
|
||||
SHOWN_TOP,
|
||||
SHOWN_TOP + INDICATOR_BAR_HEIGHT,
|
||||
SHOWN_TOP + INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT,
|
||||
],
|
||||
extrapolate: 'clamp',
|
||||
};
|
||||
|
|
@ -71,6 +71,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
|||
disableViewableItems = false;
|
||||
endIndex: number | null = null;
|
||||
indicatorBarVisible = false;
|
||||
joinCallBarVisible = false;
|
||||
currentCallBarVisible = false;
|
||||
pressed = false;
|
||||
removeViewableItemsListener: undefined | (() => void) = undefined;
|
||||
removeScrollEndIndexListener: undefined | (() => void) = undefined;
|
||||
|
|
@ -81,6 +83,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
|||
componentDidMount() {
|
||||
this.appStateListener = AppState.addEventListener('change', this.onAppStateChange);
|
||||
EventEmitter.on(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
|
||||
EventEmitter.on(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
|
||||
EventEmitter.on(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
|
||||
this.removeViewableItemsListener = this.props.registerViewableItemsListener(this.onViewableItemsChanged);
|
||||
this.removeScrollEndIndexListener = this.props.registerScrollEndIndexListener(this.onScrollEndIndex);
|
||||
}
|
||||
|
|
@ -88,6 +92,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
|||
componentWillUnmount() {
|
||||
this.appStateListener?.remove();
|
||||
EventEmitter.off(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
|
||||
EventEmitter.off(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
|
||||
EventEmitter.off(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
|
||||
if (this.removeViewableItemsListener) {
|
||||
this.removeViewableItemsListener();
|
||||
}
|
||||
|
|
@ -144,8 +150,22 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
|||
|
||||
onIndicatorBarVisible = (indicatorVisible: boolean) => {
|
||||
this.indicatorBarVisible = indicatorVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
onCurrentCallBarVisible = (currentCallVisible: boolean) => {
|
||||
this.currentCallBarVisible = currentCallVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
onJoinCallBarVisible = (joinCallVisible: boolean) => {
|
||||
this.joinCallBarVisible = joinCallVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
animateButton = () => {
|
||||
if (this.buttonVisible) {
|
||||
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
|
||||
const toValue = MAX_INPUT - this.getBarsFactor();
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
|
|
@ -169,18 +189,22 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
|||
show = () => {
|
||||
if (!this.buttonVisible && this.state.moreText && !this.props.deepLinkURL && !this.canceled && this.props.unreadCount > 0) {
|
||||
this.buttonVisible = true;
|
||||
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
this.animateButton();
|
||||
}
|
||||
}
|
||||
|
||||
getBarsFactor = () => {
|
||||
return Math.abs((
|
||||
(this.indicatorBarVisible ? 0 : INDICATOR_BAR_HEIGHT) +
|
||||
(this.joinCallBarVisible ? 0 : JOIN_CALL_BAR_HEIGHT) +
|
||||
(this.currentCallBarVisible ? 0 : CURRENT_CALL_BAR_HEIGHT)
|
||||
) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
if (this.buttonVisible) {
|
||||
this.buttonVisible = false;
|
||||
const toValue = this.indicatorBarVisible ? MIN_INPUT : MIN_INPUT + INDICATOR_BAR_FACTOR;
|
||||
const toValue = MIN_INPUT + this.getBarsFactor();
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {showPermalink} from '@actions/views/permalink';
|
|||
import {THREAD} from '@constants/screen';
|
||||
import {removePost} from '@mm-redux/actions/posts';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getPost, isRootPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getMyPreferences, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
|
@ -49,6 +49,7 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
|
||||
const isConsecutivePost = post && previousPost && !author?.is_bot && !isRootPost(state, post.id) && areConsecutivePosts(post, previousPost);
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
let isFirstReply = true;
|
||||
let isLastReply = true;
|
||||
let canDelete = false;
|
||||
|
|
@ -97,6 +98,7 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||
teammateNameDisplay,
|
||||
thread,
|
||||
threadStarter: getUser(state, post.user_id),
|
||||
callsFeatureEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {UserThread} from '@mm-redux/types/threads';
|
|||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {fromAutoResponder, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils';
|
||||
import CallMessage from '@mmproducts/calls/components/call_message';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ type PostProps = {
|
|||
theme: Theme;
|
||||
thread: UserThread;
|
||||
threadStarter: UserProfile;
|
||||
callsFeatureEnabled: boolean;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -113,7 +115,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
const Post = ({
|
||||
canDelete, collapsedThreadsEnabled, enablePostUsernameOverride, highlight, highlightPinnedOrFlagged = true, intl, isConsecutivePost, isFirstReply, isFlagged, isLastReply,
|
||||
location, post, removePost, rootPostAuthor, shouldRenderReplyButton, skipFlaggedHeader, skipPinnedHeader, showAddReaction = true, showPermalink, style,
|
||||
teammateNameDisplay, testID, theme, thread, threadStarter,
|
||||
teammateNameDisplay, testID, theme, thread, threadStarter, callsFeatureEnabled,
|
||||
}: PostProps) => {
|
||||
const pressDetected = useRef(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -239,6 +241,13 @@ const Post = ({
|
|||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (post.type === 'custom_calls' && callsFeatureEnabled) {
|
||||
body = (
|
||||
<CallMessage
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<Body
|
||||
|
|
|
|||
|
|
@ -808,6 +808,240 @@ exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
|
|||
|
||||
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot when there is a call and but calls are disabled 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(40,66,123,0.5)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_id"
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={24}
|
||||
statusStyle={
|
||||
Object {
|
||||
"backgroundColor": "#1e325c",
|
||||
"borderColor": "transparent",
|
||||
}
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
type="O"
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignSelf": "center",
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
"fontFamily": "Open Sans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 13,
|
||||
"maxWidth": "80%",
|
||||
"paddingRight": 10,
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "500",
|
||||
"maxWidth": "70%",
|
||||
"opacity": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
||||
>
|
||||
display_name
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot when there is a call and calls are enabled 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(40,66,123,0.5)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_id"
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={24}
|
||||
statusStyle={
|
||||
Object {
|
||||
"backgroundColor": "#1e325c",
|
||||
"borderColor": "transparent",
|
||||
}
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
type="O"
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignSelf": "center",
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
"fontFamily": "Open Sans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 13,
|
||||
"maxWidth": "80%",
|
||||
"paddingRight": 10,
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "500",
|
||||
"maxWidth": "70%",
|
||||
"opacity": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
||||
>
|
||||
display_name
|
||||
</Text>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"marginRight": 20,
|
||||
"textAlign": "right",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot with custom status emoji 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
|
||||
import Badge from '@components/badge';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
|
@ -41,6 +42,8 @@ export default class ChannelItem extends PureComponent {
|
|||
isSearchResult: PropTypes.bool,
|
||||
viewingGlobalThreads: PropTypes.bool,
|
||||
customStatusEnabled: PropTypes.bool.isRequired,
|
||||
channelHasCall: PropTypes.bool.isRequired,
|
||||
callsFeatureEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -214,6 +217,13 @@ export default class ChannelItem extends PureComponent {
|
|||
</Text>
|
||||
{customStatus}
|
||||
{badge}
|
||||
{this.props.callsFeatureEnabled && this.props.channelHasCall &&
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.hasCall}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
|
|
@ -288,5 +298,11 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
muted: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
hasCall: {
|
||||
color: theme.sidebarText,
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
marginRight: 20,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ describe('ChannelItem', () => {
|
|||
isSearchResult: false,
|
||||
isBot: false,
|
||||
customStatusEnabled: true,
|
||||
channelHasCall: false,
|
||||
callsFeatureEnabled: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
|
@ -50,6 +52,34 @@ describe('ChannelItem', () => {
|
|||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is a call and calls are enabled', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
callsFeatureEnabled: true,
|
||||
channelHasCall: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelItem {...newProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is a call and but calls are disabled', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
callsFeatureEnabled: false,
|
||||
channelHasCall: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelItem {...newProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with mentions and muted', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import {
|
|||
makeGetChannel,
|
||||
shouldHideDefaultChannel,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getMsgCountInChannel, getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {getCalls} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {isCustomStatusEnabled} from '@selectors/custom_status';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
import {getDraftForChannel} from '@selectors/views';
|
||||
|
|
@ -31,6 +33,7 @@ function makeMapStateToProps() {
|
|||
const currentUserId = getCurrentUserId(state);
|
||||
const channelDraft = getDraftForChannel(state, channel.id);
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const channelHasCall = Boolean(getCalls(state)[ownProps.channelId]);
|
||||
|
||||
let displayName = channel.display_name;
|
||||
let isGuest = false;
|
||||
|
|
@ -72,6 +75,7 @@ function makeMapStateToProps() {
|
|||
if (member && member.notify_props) {
|
||||
showUnreadForMsgs = member.notify_props.mark_unread !== General.MENTION;
|
||||
}
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
|
||||
const viewingGlobalThreads = getViewingGlobalThreads(state);
|
||||
return {
|
||||
|
|
@ -92,6 +96,8 @@ function makeMapStateToProps() {
|
|||
unreadMsgs,
|
||||
viewingGlobalThreads,
|
||||
customStatusEnabled: isCustomStatusEnabled(state),
|
||||
channelHasCall,
|
||||
callsFeatureEnabled,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ export const NotificationLevels = {
|
|||
|
||||
export const NOTIFY_ALL_MEMBERS = 5;
|
||||
export const INDICATOR_BAR_HEIGHT = 38;
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
|
||||
export const CHANNEL_ITEM_LARGE_BADGE_MAX_WIDTH = 38;
|
||||
export const CHANNEL_ITEM_SMALL_BADGE_MAX_WIDTH = 32;
|
||||
|
|
@ -107,6 +109,9 @@ const ViewTypes = keyMirror({
|
|||
VIEWING_GLOBAL_THREADS_ALL: null,
|
||||
|
||||
THREAD_LAST_VIEWED_AT: null,
|
||||
|
||||
JOIN_CALL_BAR_VISIBLE: null,
|
||||
CURRENT_CALL_BAR_VISIBLE: null,
|
||||
});
|
||||
|
||||
const RequiredServer = {
|
||||
|
|
|
|||
|
|
@ -51,5 +51,16 @@ const WebsocketEvents = {
|
|||
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
|
||||
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',
|
||||
SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated',
|
||||
CALLS_CHANNEL_ENABLED: 'custom_com.mattermost.calls_channel_enable_voice',
|
||||
CALLS_CHANNEL_DISABLED: 'custom_com.mattermost.calls_channel_disable_voice',
|
||||
CALLS_USER_CONNECTED: 'custom_com.mattermost.calls_user_connected',
|
||||
CALLS_USER_DISCONNECTED: 'custom_com.mattermost.calls_user_disconnected',
|
||||
CALLS_USER_MUTED: 'custom_com.mattermost.calls_user_muted',
|
||||
CALLS_USER_UNMUTED: 'custom_com.mattermost.calls_user_unmuted',
|
||||
CALLS_USER_VOICE_ON: 'custom_com.mattermost.calls_user_voice_on',
|
||||
CALLS_USER_VOICE_OFF: 'custom_com.mattermost.calls_user_voice_off',
|
||||
CALLS_CALL_START: 'custom_com.mattermost.calls_call_start',
|
||||
CALLS_SCREEN_ON: 'custom_com.mattermost.calls_user_screen_on',
|
||||
CALLS_SCREEN_OFF: 'custom_com.mattermost.calls_user_screen_off',
|
||||
};
|
||||
export default WebsocketEvents;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
|
||||
import AppsTypes from './apps';
|
||||
import BotTypes from './bots';
|
||||
import ChannelCategoryTypes from './channel_categories';
|
||||
|
|
@ -43,4 +45,5 @@ export {
|
|||
ThreadTypes,
|
||||
RemoteClusterTypes,
|
||||
AppsTypes,
|
||||
CallsTypes,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as calls from '@mmproducts/calls/store/actions/calls';
|
||||
|
||||
import * as bots from './bots';
|
||||
import * as channels from './channels';
|
||||
import * as emojis from './emojis';
|
||||
|
|
@ -37,5 +39,6 @@ export {
|
|||
timezone,
|
||||
users,
|
||||
remoteCluster,
|
||||
calls,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import calls from '@mmproducts/calls/store/reducers/calls';
|
||||
|
||||
import apps from './apps';
|
||||
import bots from './bots';
|
||||
import channelCategories from './channel_categories';
|
||||
|
|
@ -43,4 +45,5 @@ export default combineReducers({
|
|||
threads,
|
||||
remoteCluster,
|
||||
apps,
|
||||
calls,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as calls from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
import * as actions from './actions';
|
||||
import * as bots from './bots';
|
||||
import * as channels from './channels';
|
||||
|
|
@ -32,6 +34,7 @@ export {
|
|||
bots,
|
||||
plugins,
|
||||
store,
|
||||
calls,
|
||||
channels,
|
||||
errors,
|
||||
emojis,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export type PostType = 'system_add_remove' |
|
|||
'system_join_leave' |
|
||||
'system_leave_channel' |
|
||||
'system_purpose_change' |
|
||||
'system_remove_from_channel';
|
||||
'system_remove_from_channel' |
|
||||
'custom_calls';
|
||||
|
||||
export type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {CallsState} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
import {AppsState} from './apps';
|
||||
import {Bot} from './bots';
|
||||
import {ChannelCategoriesState} from './channel_categories';
|
||||
|
|
@ -58,6 +60,7 @@ export type GlobalState = {
|
|||
};
|
||||
};
|
||||
apps: AppsState;
|
||||
calls: CallsState;
|
||||
};
|
||||
errors: Array<any>;
|
||||
requests: {
|
||||
|
|
|
|||
35
app/products/calls/client/rest.ts
Normal file
35
app/products/calls/client/rest.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ServerChannelState} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
export interface ClientCallsMix {
|
||||
getCalls: () => Promise<ServerChannelState[]>;
|
||||
enableChannelCalls: (channelId: string) => Promise<ServerChannelState>;
|
||||
disableChannelCalls: (channelId: string) => Promise<ServerChannelState>;
|
||||
}
|
||||
|
||||
const ClientCalls = (superclass: any) => class extends superclass {
|
||||
getCalls = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/channels`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
enableChannelCalls = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/${channelId}`,
|
||||
{method: 'post', body: JSON.stringify({enabled: true})},
|
||||
);
|
||||
};
|
||||
|
||||
disableChannelCalls = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/${channelId}`,
|
||||
{method: 'post', body: JSON.stringify({enabled: false})},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientCalls;
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallAvatar should match snapshot muted 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.16)",
|
||||
"borderRadius": 28,
|
||||
"height": 56,
|
||||
"marginRight": 4,
|
||||
"padding": 4,
|
||||
"width": 56,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.24)",
|
||||
"borderRadius": 24,
|
||||
"height": 48,
|
||||
"padding": 4,
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": 20,
|
||||
"height": 40,
|
||||
"width": 40,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={40}
|
||||
userId="user-id"
|
||||
/>
|
||||
<CompassIcon
|
||||
name="microphone-off"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "black",
|
||||
"borderColor": "black",
|
||||
"borderRadius": 12,
|
||||
"borderWidth": 2,
|
||||
"bottom": -5,
|
||||
"color": "white",
|
||||
"height": 24,
|
||||
"overflow": "hidden",
|
||||
"padding": 2,
|
||||
"position": "absolute",
|
||||
"right": -5,
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
"width": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`CallAvatar should match snapshot size large 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.16)",
|
||||
"borderRadius": 44,
|
||||
"height": 88,
|
||||
"marginRight": 4,
|
||||
"padding": 4,
|
||||
"width": 88,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.24)",
|
||||
"borderRadius": 40,
|
||||
"height": 80,
|
||||
"padding": 4,
|
||||
"width": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": 36,
|
||||
"height": 72,
|
||||
"width": 72,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={72}
|
||||
userId="user-id"
|
||||
/>
|
||||
<CompassIcon
|
||||
name="microphone"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#3DB887",
|
||||
"borderColor": "black",
|
||||
"borderRadius": 12,
|
||||
"borderWidth": 2,
|
||||
"bottom": -5,
|
||||
"color": "white",
|
||||
"height": 24,
|
||||
"overflow": "hidden",
|
||||
"padding": 2,
|
||||
"position": "absolute",
|
||||
"right": -5,
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
"width": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`CallAvatar should match snapshot unmuted 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.16)",
|
||||
"borderRadius": 28,
|
||||
"height": 56,
|
||||
"marginRight": 4,
|
||||
"padding": 4,
|
||||
"width": 56,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(255,255,255,0.24)",
|
||||
"borderRadius": 24,
|
||||
"height": 48,
|
||||
"padding": 4,
|
||||
"width": 48,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": 20,
|
||||
"height": 40,
|
||||
"width": 40,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={40}
|
||||
userId="user-id"
|
||||
/>
|
||||
<CompassIcon
|
||||
name="microphone"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#3DB887",
|
||||
"borderColor": "black",
|
||||
"borderRadius": 12,
|
||||
"borderWidth": 2,
|
||||
"bottom": -5,
|
||||
"color": "white",
|
||||
"height": 24,
|
||||
"overflow": "hidden",
|
||||
"padding": 2,
|
||||
"position": "absolute",
|
||||
"right": -5,
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
"width": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallDuration should match snapshot 1`] = `
|
||||
<Text
|
||||
style={Object {}}
|
||||
>
|
||||
00:15
|
||||
</Text>
|
||||
`;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EnableDisableCalls should match snapshot if calls are disabled 1`] = `
|
||||
<React.Fragment>
|
||||
<Separator
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<channelInfoRow
|
||||
action={[Function]}
|
||||
defaultMessage="Enable Calls"
|
||||
icon="phone-outline"
|
||||
rightArrow={false}
|
||||
shouldRender={true}
|
||||
testID="test-id"
|
||||
textId="mobile.channel_info.enable_calls"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
togglable={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`EnableDisableCalls should match snapshot if calls are enabled 1`] = `
|
||||
<React.Fragment>
|
||||
<Separator
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<channelInfoRow
|
||||
action={[Function]}
|
||||
defaultMessage="Disable Calls"
|
||||
icon="phone-outline"
|
||||
rightArrow={false}
|
||||
shouldRender={true}
|
||||
testID="test-id"
|
||||
textId="mobile.channel_info.disable_calls"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
togglable={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FloatingCallContainer should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"top": 91,
|
||||
"width": "100%",
|
||||
"zIndex": 9,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
test
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
36
app/products/calls/components/call_avatar.test.js
Normal file
36
app/products/calls/components/call_avatar.test.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import CallAvatar from './call_avatar';
|
||||
|
||||
describe('CallAvatar', () => {
|
||||
const baseProps = {
|
||||
userId: 'user-id',
|
||||
volume: 1,
|
||||
muted: false,
|
||||
size: 'm',
|
||||
};
|
||||
|
||||
test('should match snapshot unmuted', () => {
|
||||
const wrapper = shallow(<CallAvatar {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot muted', () => {
|
||||
const props = {...baseProps, muted: true};
|
||||
const wrapper = shallow(<CallAvatar {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot size large', () => {
|
||||
const props = {...baseProps, size: 'l'};
|
||||
const wrapper = shallow(<CallAvatar {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
82
app/products/calls/components/call_avatar.tsx
Normal file
82
app/products/calls/components/call_avatar.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View, StyleSheet} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
volume: number;
|
||||
muted?: boolean;
|
||||
size?: 'm' | 'l';
|
||||
}
|
||||
|
||||
const getStyleSheet = (props: Props) => {
|
||||
const baseSize = props.size === 'm' || !props.size ? 40 : 72;
|
||||
|
||||
return StyleSheet.create({
|
||||
pictureHalo: {
|
||||
backgroundColor: 'rgba(255,255,255,' + (0.16 * props.volume) + ')',
|
||||
height: baseSize + 16,
|
||||
width: baseSize + 16,
|
||||
padding: 4,
|
||||
marginRight: 4,
|
||||
borderRadius: (baseSize + 16) / 2,
|
||||
},
|
||||
pictureHalo2: {
|
||||
backgroundColor: 'rgba(255,255,255,' + (0.24 * props.volume) + ')',
|
||||
height: baseSize + 8,
|
||||
width: baseSize + 8,
|
||||
padding: 4,
|
||||
borderRadius: (baseSize + 8) / 2,
|
||||
},
|
||||
picture: {
|
||||
borderRadius: baseSize / 2,
|
||||
height: baseSize,
|
||||
width: baseSize,
|
||||
},
|
||||
mute: {
|
||||
position: 'absolute',
|
||||
bottom: -5,
|
||||
right: -5,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
backgroundColor: props.muted ? 'black' : '#3DB887',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const CallAvatar = (props: Props) => {
|
||||
const style = getStyleSheet(props);
|
||||
return (
|
||||
<View style={style.pictureHalo}>
|
||||
<View style={style.pictureHalo2}>
|
||||
<View style={style.picture}>
|
||||
<ProfilePicture
|
||||
userId={props.userId}
|
||||
size={props.size === 'm' || !props.size ? 40 : 72}
|
||||
showStatus={false}
|
||||
/>
|
||||
{props.muted !== undefined &&
|
||||
<CompassIcon
|
||||
name={props.muted ? 'microphone-off' : 'microphone'}
|
||||
size={16}
|
||||
style={style.mute}
|
||||
/>}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default CallAvatar;
|
||||
63
app/products/calls/components/call_duration.test.js
Normal file
63
app/products/calls/components/call_duration.test.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import CallDuration from './call_duration';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: (f) => f(),
|
||||
}));
|
||||
|
||||
describe('CallDuration', () => {
|
||||
const baseProps = {
|
||||
style: {},
|
||||
value: moment.now() - 15000,
|
||||
updateIntervalInSeconds: 10000,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<CallDuration {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot more in the past', () => {
|
||||
const props = {...baseProps, value: moment.now() - ((10 * 60 * 60 * 1000) + (30 * 60 * 1000) + (25 * 1000) + 500)};
|
||||
const wrapper = shallow(<CallDuration {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('10:30:25');
|
||||
});
|
||||
|
||||
test('should match snapshot more in the future', () => {
|
||||
const props = {...baseProps, value: moment.now() + 15500};
|
||||
const wrapper = shallow(<CallDuration {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('00:00');
|
||||
});
|
||||
|
||||
test('should re-render after updateIntervalInSeconds', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {...baseProps, value: moment.now(), updateIntervalInSeconds: 10};
|
||||
const wrapper = shallow(<CallDuration {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('00:00');
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(wrapper.getElement().props.children).toBe('00:00');
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(wrapper.getElement().props.children).toBe('00:10');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should not re-render if updateIntervalInSeconds is not passed', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {value: moment.now()};
|
||||
const wrapper = shallow(<CallDuration {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('00:00');
|
||||
jest.advanceTimersByTime(500000000);
|
||||
expect(wrapper.getElement().props.children).toBe('00:00');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
52
app/products/calls/components/call_duration.tsx
Normal file
52
app/products/calls/components/call_duration.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Text, StyleProp, TextStyle} from 'react-native';
|
||||
|
||||
type CallDurationProps = {
|
||||
style: StyleProp<TextStyle>;
|
||||
value: number;
|
||||
updateIntervalInSeconds?: number;
|
||||
}
|
||||
|
||||
const CallDuration = ({value, style, updateIntervalInSeconds}: CallDurationProps) => {
|
||||
const getCallDuration = () => {
|
||||
const now = moment();
|
||||
const startTime = moment(value);
|
||||
if (now < startTime) {
|
||||
return '00:00';
|
||||
}
|
||||
|
||||
const totalSeconds = now.diff(startTime, 'seconds');
|
||||
const seconds = totalSeconds % 60;
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
|
||||
}
|
||||
return `${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
|
||||
};
|
||||
|
||||
const [formattedTime, setFormattedTime] = useState(getCallDuration());
|
||||
useEffect(() => {
|
||||
if (updateIntervalInSeconds) {
|
||||
const interval = setInterval(() => setFormattedTime(getCallDuration()), updateIntervalInSeconds * 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => null;
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
<Text style={style}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallDuration;
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallMessage should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"flexDirection": "row",
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"paddingBottom": 5,
|
||||
"paddingTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
"overflow": "hidden",
|
||||
"padding": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="{user} started a call"
|
||||
id="call_message.started_a_call"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"user": "User 1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedRelativeTime
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
}
|
||||
}
|
||||
updateIntervalInSeconds={1}
|
||||
value={100}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-outline"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Join Call"
|
||||
id="call_message.join_call"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`CallMessage should match snapshot for ended call 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"flexDirection": "row",
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"paddingBottom": 5,
|
||||
"paddingTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-hangup"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(63,67,80,0.6)",
|
||||
"borderRadius": 8,
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
"overflow": "hidden",
|
||||
"padding": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Call ended"
|
||||
id="call_message.call_ended"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Ended at {time}"
|
||||
id="call_message.call_ended_at"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"time": <FormattedTime
|
||||
isMilitaryTime={false}
|
||||
timezone="utc"
|
||||
value={200}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"marginLeft": 5,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
•
|
||||
</Text>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Lasted {duration}"
|
||||
id="call_message.call_lasted"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"duration": "a few seconds",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`CallMessage should match snapshot for the call already in the current channel 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
"flexDirection": "row",
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"paddingBottom": 5,
|
||||
"paddingTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
"overflow": "hidden",
|
||||
"padding": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="{user} started a call"
|
||||
id="call_message.started_a_call"
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"user": "User 1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedRelativeTime
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
}
|
||||
}
|
||||
updateIntervalInSeconds={1}
|
||||
value={100}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": "center",
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#339970",
|
||||
"borderRadius": 8,
|
||||
"flexDirection": "row",
|
||||
"padding": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-outline"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Current Call"
|
||||
id="call_message.current_call"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, TouchableOpacity} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {shallowWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import CallMessage from './call_message';
|
||||
|
||||
describe('CallMessage', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
joinCall: jest.fn(),
|
||||
},
|
||||
theme: Preferences.THEMES.denim,
|
||||
post: {
|
||||
props: {
|
||||
start_at: 100,
|
||||
},
|
||||
type: 'custom_calls',
|
||||
},
|
||||
user: {
|
||||
id: 'user-1-id',
|
||||
username: 'user-1-username',
|
||||
nickname: 'User 1',
|
||||
},
|
||||
teammateNameDisplay: Preferences.DISPLAY_PREFER_NICKNAME,
|
||||
confirmToJoin: false,
|
||||
isMilitaryTime: false,
|
||||
userTimezone: 'utc',
|
||||
currentChannelName: 'Current Channel',
|
||||
callChannelName: 'Call Channel',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(<CallMessage {...baseProps}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for the call already in the current channel', () => {
|
||||
const props = {...baseProps, alreadyInTheCall: true};
|
||||
const wrapper = shallowWithIntl(<CallMessage {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for ended call', () => {
|
||||
const props = {...baseProps, post: {...baseProps.post, props: {start_at: 100, end_at: 200}}};
|
||||
const wrapper = shallowWithIntl(<CallMessage {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should join on click join button', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<CallMessage {...props}/>).dive();
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should ask for confirmation on click join button', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, confirmToJoin: true, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<CallMessage {...props}/>).dive();
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not ask or join on click current call button if I am in the current call', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, actions: {joinCall}, alreadyInTheCall: true};
|
||||
const wrapper = shallowWithIntl(<CallMessage {...props}/>).dive();
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
204
app/products/calls/components/call_message/call_message.tsx
Normal file
204
app/products/calls/components/call_message/call_message.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useCallback} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {View, TouchableOpacity, Text} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedRelativeTime from '@components/formatted_relative_time';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import FormattedTime from '@components/formatted_time';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
type CallMessageProps = {
|
||||
actions: {
|
||||
joinCall: (channelId: string) => void;
|
||||
};
|
||||
post: Post;
|
||||
user: UserProfile;
|
||||
theme: Theme;
|
||||
teammateNameDisplay: string;
|
||||
confirmToJoin: boolean;
|
||||
alreadyInTheCall: boolean;
|
||||
isMilitaryTime: boolean;
|
||||
userTimezone: string;
|
||||
currentChannelName: string;
|
||||
callChannelName: string;
|
||||
intl: typeof IntlShape;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
messageStyle: {
|
||||
flexDirection: 'row',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
messageText: {
|
||||
flex: 1,
|
||||
},
|
||||
joinCallIcon: {
|
||||
padding: 12,
|
||||
backgroundColor: '#339970',
|
||||
borderRadius: 8,
|
||||
marginRight: 5,
|
||||
color: 'white',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
phoneHangupIcon: {
|
||||
padding: 12,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
borderRadius: 8,
|
||||
marginRight: 5,
|
||||
color: 'white',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
joinCallButtonText: {
|
||||
color: 'white',
|
||||
},
|
||||
joinCallButtonIcon: {
|
||||
color: 'white',
|
||||
marginRight: 5,
|
||||
},
|
||||
startedText: {
|
||||
color: theme.centerChannelColor,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
joinCallButton: {
|
||||
flexDirection: 'row',
|
||||
padding: 12,
|
||||
backgroundColor: '#339970',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
endCallInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
},
|
||||
separator: {
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInTheCall, theme, actions, userTimezone, isMilitaryTime, currentChannelName, callChannelName, intl}: CallMessageProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const joinHandler = useCallback(() => {
|
||||
if (alreadyInTheCall) {
|
||||
return;
|
||||
}
|
||||
leaveAndJoinWithAlert(intl, post.channel_id, callChannelName, currentChannelName, confirmToJoin, actions.joinCall);
|
||||
}, [post.channel_id, callChannelName, currentChannelName, confirmToJoin, actions.joinCall]);
|
||||
|
||||
if (post.props.end_at) {
|
||||
return (
|
||||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={16}
|
||||
style={style.phoneHangupIcon}
|
||||
/>
|
||||
<View style={style.messageText}>
|
||||
<FormattedText
|
||||
id='call_message.call_ended'
|
||||
defaultMessage='Call ended'
|
||||
style={style.startedText}
|
||||
/>
|
||||
<View
|
||||
style={style.endCallInfo}
|
||||
>
|
||||
<FormattedText
|
||||
id='call_message.call_ended_at'
|
||||
defaultMessage='Ended at {time}'
|
||||
style={style.timeText}
|
||||
values={{
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={post.props.end_at}
|
||||
isMilitaryTime={isMilitaryTime}
|
||||
timezone={userTimezone}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Text style={style.separator}>{'•'}</Text>
|
||||
<FormattedText
|
||||
style={style.timeText}
|
||||
id='call_message.call_lasted'
|
||||
defaultMessage='Lasted {duration}'
|
||||
values={{
|
||||
duration: moment.duration(post.props.end_at - post.props.start_at).humanize(false),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<View style={style.messageText}>
|
||||
<FormattedText
|
||||
id='call_message.started_a_call'
|
||||
defaultMessage='{user} started a call'
|
||||
values={{user: displayUsername(user, teammateNameDisplay)}}
|
||||
style={style.startedText}
|
||||
/>
|
||||
<FormattedRelativeTime
|
||||
value={post.props.start_at}
|
||||
updateIntervalInSeconds={1}
|
||||
style={style.timeText}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={style.joinCallButton}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-outline'
|
||||
size={16}
|
||||
style={style.joinCallButtonIcon}
|
||||
/>
|
||||
{alreadyInTheCall &&
|
||||
<FormattedText
|
||||
id='call_message.current_call'
|
||||
defaultMessage='Current Call'
|
||||
style={style.joinCallButtonText}
|
||||
/>}
|
||||
{!alreadyInTheCall &&
|
||||
<FormattedText
|
||||
id='call_message.join_call'
|
||||
defaultMessage='Join Call'
|
||||
style={style.joinCallButtonText}
|
||||
/>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(CallMessage);
|
||||
55
app/products/calls/components/call_message/index.ts
Normal file
55
app/products/calls/components/call_message/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getBool, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getUser, getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
|
||||
import {joinCall} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import CallMessage from './call_message';
|
||||
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
|
||||
type OwnProps = {
|
||||
post: Post;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const {post} = ownProps;
|
||||
const user = getUser(state, post.user_id);
|
||||
const currentUser = getCurrentUser(state);
|
||||
const currentCall = getCurrentCall(state);
|
||||
const call = getCalls(state)[post.channel_id];
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
|
||||
return {
|
||||
user,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
confirmToJoin: Boolean(currentCall && call && currentCall.channelId !== call.channelId),
|
||||
alreadyInTheCall: Boolean(currentCall && call && currentCall.channelId === call.channelId),
|
||||
isMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time'),
|
||||
userTimezone: enableTimezone ? getUserCurrentTimezone(currentUser.timezone) : undefined,
|
||||
currentChannelName: getChannel(state, post.channel_id)?.display_name,
|
||||
callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '',
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
joinCall,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CallMessage);
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CurrentCall should match snapshot muted 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#3F4350",
|
||||
"borderRadius": 5,
|
||||
"flexDirection": "row",
|
||||
"height": 64,
|
||||
"padding": 4,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CallAvatar
|
||||
userId="user-1-id"
|
||||
volume={0.5}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="{username} is speaking"
|
||||
id="current_call.user-is-speaking"
|
||||
values={
|
||||
Object {
|
||||
"username": "User 1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.64,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="~{channelName}"
|
||||
id="current_call.channel-name"
|
||||
values={
|
||||
Object {
|
||||
"channelName": "Channel Name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="arrow-expand"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginRight": 8,
|
||||
"padding": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="microphone-off"
|
||||
size={24}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#3DB887",
|
||||
"borderRadius": 4,
|
||||
"color": "#ffffff",
|
||||
"height": 42,
|
||||
"justifyContent": "center",
|
||||
"margin": 4,
|
||||
"overflow": "hidden",
|
||||
"padding": 9,
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
"width": 42,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`CurrentCall should match snapshot unmuted 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#3F4350",
|
||||
"borderRadius": 5,
|
||||
"flexDirection": "row",
|
||||
"height": 64,
|
||||
"padding": 4,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CallAvatar
|
||||
userId="user-1-id"
|
||||
volume={0.5}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="{username} is speaking"
|
||||
id="current_call.user-is-speaking"
|
||||
values={
|
||||
Object {
|
||||
"username": "User 1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.64,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="~{channelName}"
|
||||
id="current_call.channel-name"
|
||||
values={
|
||||
Object {
|
||||
"channelName": "Channel Name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="arrow-expand"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginRight": 8,
|
||||
"padding": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="microphone"
|
||||
size={24}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#3DB887",
|
||||
"borderRadius": 4,
|
||||
"color": "#ffffff",
|
||||
"height": 42,
|
||||
"justifyContent": "center",
|
||||
"margin": 4,
|
||||
"overflow": "hidden",
|
||||
"padding": 9,
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
"width": 42,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import CurrentCall from './current_call';
|
||||
|
||||
describe('CurrentCall', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
muteMyself: jest.fn(),
|
||||
unmuteMyself: jest.fn(),
|
||||
},
|
||||
theme: Preferences.THEMES.denim,
|
||||
channel: {
|
||||
display_name: 'Channel Name',
|
||||
},
|
||||
speaker: {
|
||||
id: 'user-1-id',
|
||||
muted: false,
|
||||
isTalking: true,
|
||||
},
|
||||
speakerUser: {
|
||||
id: 'user-1-id',
|
||||
username: 'user-1-username',
|
||||
nickname: 'User 1',
|
||||
},
|
||||
call: {
|
||||
participants: {
|
||||
'user-1-id': {
|
||||
id: 'user-1-id',
|
||||
muted: false,
|
||||
isTalking: false,
|
||||
},
|
||||
'user-2-id': {
|
||||
id: 'user-2-id',
|
||||
muted: true,
|
||||
isTalking: true,
|
||||
},
|
||||
},
|
||||
channelId: 'channel-id',
|
||||
startTime: 100,
|
||||
speakers: 'user-2-id',
|
||||
screenOn: false,
|
||||
threadId: false,
|
||||
},
|
||||
currentParticipant: {
|
||||
id: 'user-2-id',
|
||||
muted: true,
|
||||
isTalking: true,
|
||||
},
|
||||
teammateNameDisplay: Preferences.DISPLAY_PREFER_NICKNAME,
|
||||
};
|
||||
|
||||
test('should match snapshot muted', () => {
|
||||
const props = {...baseProps, currentParticipant: {...baseProps.currentParticipant, muted: true}};
|
||||
const wrapper = shallow(<CurrentCall {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot unmuted', () => {
|
||||
const props = {...baseProps, currentParticipant: {...baseProps.currentParticipant, muted: false}};
|
||||
const wrapper = shallow(<CurrentCall {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should mute on click mute button', () => {
|
||||
const muteMyself = jest.fn();
|
||||
const unmuteMyself = jest.fn();
|
||||
const props = {...baseProps, actions: {muteMyself, unmuteMyself}, currentParticipant: {...baseProps.currentParticipant, muted: false}};
|
||||
const wrapper = shallow(<CurrentCall {...props}/>);
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
expect(props.actions.muteMyself).toHaveBeenCalled();
|
||||
expect(props.actions.unmuteMyself).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should ask for confirmation on click', () => {
|
||||
const muteMyself = jest.fn();
|
||||
const unmuteMyself = jest.fn();
|
||||
const props = {...baseProps, actions: {unmuteMyself, muteMyself}, currentParticipant: {...baseProps.currentParticipant, muted: true}};
|
||||
const wrapper = shallow(<CurrentCall {...props}/>);
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
expect(props.actions.muteMyself).not.toHaveBeenCalled();
|
||||
expect(props.actions.unmuteMyself).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
175
app/products/calls/components/current_call/current_call.tsx
Normal file
175
app/products/calls/components/current_call/current_call.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
import {Options} from 'react-native-navigation';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ViewTypes, {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import CallAvatar from '@mmproducts/calls/components/call_avatar';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
import type {Call, CallParticipant} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
muteMyself: (channelId: string) => GenericAction;
|
||||
unmuteMyself: (channelId: string) => GenericAction;
|
||||
};
|
||||
theme: Theme;
|
||||
channel: Channel;
|
||||
speaker: CallParticipant;
|
||||
speakerUser: UserProfile;
|
||||
call: Call;
|
||||
currentParticipant: CallParticipant;
|
||||
teammateNameDisplay: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
wrapper: {
|
||||
padding: 10,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3F4350',
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 4,
|
||||
height: CURRENT_CALL_BAR_HEIGHT - 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
speakingUser: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
},
|
||||
currentChannel: {
|
||||
color: theme.sidebarText,
|
||||
opacity: 0.64,
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
width: 42,
|
||||
height: 42,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#3DB887',
|
||||
borderRadius: 4,
|
||||
margin: 4,
|
||||
padding: 9,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
muted: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
expandIcon: {
|
||||
color: theme.sidebarText,
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CurrentCall = (props: Props) => {
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(ViewTypes.CURRENT_CALL_BAR_VISIBLE, Boolean(props.call));
|
||||
return () => {
|
||||
EventEmitter.emit(ViewTypes.CURRENT_CALL_BAR_VISIBLE, Boolean(false));
|
||||
};
|
||||
}, [props.call]);
|
||||
|
||||
const goToCallScreen = useCallback(() => {
|
||||
const options: Options = {
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
componentBackgroundColor: '#000',
|
||||
orientation: ['portrait', 'landscape'],
|
||||
},
|
||||
topBar: {
|
||||
background: {
|
||||
color: '#000',
|
||||
},
|
||||
visible: Platform.OS === 'android',
|
||||
},
|
||||
};
|
||||
goToScreen('Call', 'Call', {}, options);
|
||||
}, []);
|
||||
|
||||
const muteUnmute = useCallback(() => {
|
||||
if (props.currentParticipant?.muted) {
|
||||
props.actions.unmuteMyself(props.call.channelId);
|
||||
} else {
|
||||
props.actions.muteMyself(props.call.channelId);
|
||||
}
|
||||
}, [props.currentParticipant?.muted]);
|
||||
|
||||
if (!props.call) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(props.theme);
|
||||
return (
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userId={props.speaker?.id}
|
||||
volume={props.speaker?.isTalking ? 0.5 : 0}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>
|
||||
<FormattedText
|
||||
id='current_call.user-is-speaking'
|
||||
defaultMessage='{username} is speaking'
|
||||
values={{username: displayUsername(props.speakerUser, props.teammateNameDisplay)}}
|
||||
/>
|
||||
</Text>
|
||||
<Text style={style.currentChannel}>
|
||||
<FormattedText
|
||||
id='current_call.channel-name'
|
||||
defaultMessage='~{channelName}'
|
||||
values={{channelName: props.channel.display_name}}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name={props.currentParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={[style.micIcon, props.currentParticipant?.muted ? style.muted : undefined]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default CurrentCall;
|
||||
42
app/products/calls/components/current_call/index.ts
Normal file
42
app/products/calls/components/current_call/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {muteMyself, unmuteMyself} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import CurrentCall from './current_call';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentCall = getCurrentCall(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const speakerId = currentCall && currentCall.speakers && currentCall.speakers[0];
|
||||
const speaker = currentCall && ((speakerId && currentCall.participants[speakerId]) || Object.values(currentCall.participants)[0]);
|
||||
const currentParticipant = currentCall?.participants[currentUserId];
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
call: currentCall,
|
||||
speaker,
|
||||
speakerUser: speaker ? state.entities.users.profiles[speaker.id] : null,
|
||||
channel: getChannel(state, currentCall?.channelId || ''),
|
||||
currentParticipant,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
muteMyself,
|
||||
unmuteMyself,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CurrentCall);
|
||||
39
app/products/calls/components/enable_disable_calls.test.js
Normal file
39
app/products/calls/components/enable_disable_calls.test.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import EnableDisableCalls from './enable_disable_calls';
|
||||
|
||||
describe('EnableDisableCalls', () => {
|
||||
const baseProps = {
|
||||
testID: 'test-id',
|
||||
theme: Preferences.THEMES.denim,
|
||||
onPress: jest.fn(),
|
||||
canEnableDisableCalls: true,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
test('should match snapshot if calls are disabled', () => {
|
||||
const wrapper = shallow(<EnableDisableCalls {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if calls are enabled', () => {
|
||||
const props = {...baseProps, enabled: true};
|
||||
const wrapper = shallow(<EnableDisableCalls {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should be null if you can not enable/disable calls', () => {
|
||||
const props = {...baseProps, canEnableDisableCalls: false};
|
||||
const wrapper = shallow(<EnableDisableCalls {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toBeNull();
|
||||
});
|
||||
});
|
||||
45
app/products/calls/components/enable_disable_calls.tsx
Normal file
45
app/products/calls/components/enable_disable_calls.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
|
||||
import Separator from '@screens/channel_info/separator';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
theme: Theme;
|
||||
onPress: (channelId: string) => void;
|
||||
canEnableDisableCalls: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const EnableDisableCalls = (props: Props) => {
|
||||
const {testID, canEnableDisableCalls, theme, onPress, enabled} = props;
|
||||
|
||||
const handleEnableDisableCalls = useCallback(preventDoubleTap(onPress), [onPress]);
|
||||
|
||||
if (!canEnableDisableCalls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator theme={theme}/>
|
||||
<ChannelInfoRow
|
||||
testID={testID}
|
||||
action={handleEnableDisableCalls}
|
||||
defaultMessage={enabled ? 'Disable Calls' : 'Enable Calls'}
|
||||
icon='phone-outline'
|
||||
textId={enabled ? t('mobile.channel_info.disable_calls') : t('mobile.channel_info.enable_calls')}
|
||||
theme={theme}
|
||||
rightArrow={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnableDisableCalls;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import FloatingCallContainer from './floating_call_container';
|
||||
|
||||
describe('FloatingCallContainer', () => {
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FloatingCallContainer><Text>{'test'}</Text></FloatingCallContainer>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
57
app/products/calls/components/floating_call_container.tsx
Normal file
57
app/products/calls/components/floating_call_container.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {View, Platform} from 'react-native';
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const {
|
||||
IOS_TOP_PORTRAIT,
|
||||
STATUS_BAR_HEIGHT,
|
||||
} = ViewTypes;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme(() => {
|
||||
const topBarHeight = Platform.select({android: 9, ios: IOS_TOP_PORTRAIT - STATUS_BAR_HEIGHT}) || 0;
|
||||
|
||||
return {
|
||||
wrapper: {
|
||||
position: 'absolute',
|
||||
top: topBarHeight + ViewTypes.STATUS_BAR_HEIGHT + 27,
|
||||
width: '100%',
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 9,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 9,
|
||||
},
|
||||
}),
|
||||
},
|
||||
withIndicatorBar: {
|
||||
top: topBarHeight + ViewTypes.STATUS_BAR_HEIGHT + 27 + ViewTypes.INDICATOR_BAR_HEIGHT,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNodeArray;
|
||||
}
|
||||
|
||||
const FloatingCallContainer = (props: Props) => {
|
||||
const style = getStyleSheet(props);
|
||||
const [indicatorBarVisible, setIndicatorBarVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
EventEmitter.on(ViewTypes.INDICATOR_BAR_VISIBLE, setIndicatorBarVisible);
|
||||
return () => EventEmitter.off(ViewTypes.INDICATOR_BAR_VISIBLE, setIndicatorBarVisible);
|
||||
}, []);
|
||||
return (
|
||||
<View style={[style.wrapper, indicatorBarVisible ? style.withIndicatorBar : undefined]}>
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingCallContainer;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JoinCall should match snapshot 1`] = `
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#3DB887",
|
||||
"flexDirection": "row",
|
||||
"height": 38,
|
||||
"justifyContent": "center",
|
||||
"padding": 5,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "bold",
|
||||
}
|
||||
}
|
||||
>
|
||||
Join Call
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"fontWeight": "400",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedRelativeTime
|
||||
updateIntervalInSeconds={1}
|
||||
value={100}
|
||||
/>
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(Avatars)
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="CALL PARTICIPANTS"
|
||||
id="calls.join_call.participants_list_header"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.56)",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600",
|
||||
"paddingHorizontal": 16,
|
||||
"paddingVertical": 0,
|
||||
"top": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
userIds={
|
||||
Array [
|
||||
"user-1-id",
|
||||
"user-2-id",
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
`;
|
||||
37
app/products/calls/components/join_call/index.ts
Normal file
37
app/products/calls/components/join_call/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getChannel, getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {joinCall} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import JoinCall from './join_call';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const call = getCalls(state)[currentChannelId];
|
||||
const currentCall = getCurrentCall(state);
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
call: call === currentCall ? null : call,
|
||||
confirmToJoin: Boolean(currentCall && call && currentCall.channelId !== call.channelId),
|
||||
alreadyInTheCall: Boolean(currentCall && call && currentCall.channelId === call.channelId),
|
||||
currentChannelName: getChannel(state, currentChannelId)?.display_name,
|
||||
callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '',
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
joinCall,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JoinCall);
|
||||
75
app/products/calls/components/join_call/join_call.test.js
Normal file
75
app/products/calls/components/join_call/join_call.test.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {shallowWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import JoinCall from './join_call';
|
||||
|
||||
describe('JoinCall', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
joinCall: jest.fn(),
|
||||
},
|
||||
theme: Preferences.THEMES.denim,
|
||||
call: {
|
||||
participants: {
|
||||
'user-1-id': {
|
||||
id: 'user-1-id',
|
||||
muted: false,
|
||||
isTalking: false,
|
||||
},
|
||||
'user-2-id': {
|
||||
id: 'user-2-id',
|
||||
muted: true,
|
||||
isTalking: true,
|
||||
},
|
||||
},
|
||||
channelId: 'channel-id',
|
||||
startTime: 100,
|
||||
speakers: 'user-2-id',
|
||||
screenOn: false,
|
||||
threadId: false,
|
||||
},
|
||||
confirmToJoin: false,
|
||||
alreadyInTheCall: false,
|
||||
currentChannelName: 'Current Channel',
|
||||
callChannelName: 'Call Channel',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(<JoinCall {...baseProps}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show it when I am already in the call', () => {
|
||||
const props = {...baseProps, alreadyInTheCall: true};
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toBeNull();
|
||||
});
|
||||
|
||||
test('should join on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
|
||||
|
||||
wrapper.simulate('press');
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should ask for confirmation on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, confirmToJoin: true, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<JoinCall {...props}/>).dive();
|
||||
|
||||
wrapper.simulate('press');
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
132
app/products/calls/components/join_call/join_call.tsx
Normal file
132
app/products/calls/components/join_call/join_call.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {View, Text, Pressable} from 'react-native';
|
||||
|
||||
import Avatars from '@components/avatars';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedRelativeTime from '@components/formatted_relative_time';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ViewTypes, {JOIN_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {Call} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
joinCall: (channelId: string) => any;
|
||||
};
|
||||
theme: Theme;
|
||||
call: Call;
|
||||
confirmToJoin: boolean;
|
||||
alreadyInTheCall: boolean;
|
||||
currentChannelName: string;
|
||||
callChannelName: string;
|
||||
intl: typeof IntlShape;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#3DB887',
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: JOIN_CALL_BAR_HEIGHT,
|
||||
},
|
||||
joinCallIcon: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: 10,
|
||||
marginRight: 5,
|
||||
},
|
||||
joinCall: {
|
||||
color: theme.sidebarText,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16,
|
||||
},
|
||||
started: {
|
||||
flex: 1,
|
||||
color: theme.sidebarText,
|
||||
fontWeight: '400',
|
||||
marginLeft: 10,
|
||||
},
|
||||
avatars: {
|
||||
marginRight: 5,
|
||||
},
|
||||
headerText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 0,
|
||||
top: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const JoinCall = (props: Props) => {
|
||||
if (!props.call) {
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(ViewTypes.JOIN_CALL_BAR_VISIBLE, Boolean(props.call && !props.alreadyInTheCall));
|
||||
return () => {
|
||||
EventEmitter.emit(ViewTypes.JOIN_CALL_BAR_VISIBLE, Boolean(false));
|
||||
};
|
||||
}, [props.call, props.alreadyInTheCall]);
|
||||
|
||||
const joinHandler = useCallback(() => {
|
||||
leaveAndJoinWithAlert(props.intl, props.call.channelId, props.callChannelName, props.currentChannelName, props.confirmToJoin, props.actions.joinCall);
|
||||
}, [props.call.channelId, props.callChannelName, props.currentChannelName, props.confirmToJoin, props.actions.joinCall]);
|
||||
|
||||
if (props.alreadyInTheCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(props.theme);
|
||||
const userIds = useMemo(() => {
|
||||
return Object.values(props.call.participants || {}).map((x) => x.id);
|
||||
}, [props.call.participants]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={style.container}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<Text style={style.joinCall}>{'Join Call'}</Text>
|
||||
<Text style={style.started}>
|
||||
<FormattedRelativeTime
|
||||
value={props.call.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</Text>
|
||||
<View style={style.avatars}>
|
||||
<Avatars
|
||||
userIds={userIds}
|
||||
breakAt={1}
|
||||
listTitle={
|
||||
<FormattedText
|
||||
id='calls.join_call.participants_list_header'
|
||||
defaultMessage={'CALL PARTICIPANTS'}
|
||||
style={style.headerText}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
export default injectIntl(JoinCall);
|
||||
29
app/products/calls/components/leave_and_join_alert.tsx
Normal file
29
app/products/calls/components/leave_and_join_alert.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
export default function leaveAndJoinWithAlert(intl: typeof IntlShape, channelId: string, callChannelName: string, currentChannelName: string, confirmToJoin: boolean, joinCall: (channelId: string) => void) {
|
||||
if (confirmToJoin) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'calls.confirm-to-join-title', defaultMessage: 'Are you sure you want to switch to a different call?'}),
|
||||
intl.formatMessage({
|
||||
id: 'calls.confirm-to-join-description',
|
||||
defaultMessage: 'You are already on a channel call in ~{callChannelName}. Do you want to leave your current call and join the call in ~{currentChannelName}?',
|
||||
}, {callChannelName, currentChannelName}),
|
||||
[
|
||||
{
|
||||
text: intl.formatMessage({id: 'calls.confirm-to-join-cancel', defaultMessage: 'Cancel'}),
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({id: 'calls.confirm-to-join-leave-and-join', defaultMessage: 'Leave & Join'}),
|
||||
onPress: () => joinCall(channelId),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
);
|
||||
} else {
|
||||
joinCall(channelId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StartCall should match snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<Separator
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<channelInfoRow
|
||||
action={[Function]}
|
||||
defaultMessage="Start Call"
|
||||
icon="phone-in-talk"
|
||||
rightArrow={false}
|
||||
shouldRender={true}
|
||||
testID="test-id"
|
||||
textId="mobile.channel_info.start_call"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
togglable={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartCall should match snapshot when there is already an ongoing call in the channel 1`] = `
|
||||
<React.Fragment>
|
||||
<Separator
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<channelInfoRow
|
||||
action={[Function]}
|
||||
defaultMessage="Join Ongoing Call"
|
||||
icon="phone-in-talk"
|
||||
rightArrow={false}
|
||||
shouldRender={true}
|
||||
testID="test-id"
|
||||
textId="mobile.channel_info.join_ongoing_call"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
togglable={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
34
app/products/calls/components/start_call/index.ts
Normal file
34
app/products/calls/components/start_call/index.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getChannel, getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {joinCall} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getCalls, getCurrentCall} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import StartCall from './start_call';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const call = getCalls(state)[currentChannelId];
|
||||
const currentCall = getCurrentCall(state);
|
||||
return {
|
||||
confirmToJoin: Boolean(currentCall && currentCall.channelId !== currentChannelId),
|
||||
alreadyInTheCall: Boolean(currentCall && call && currentCall.channelId === call.channelId),
|
||||
callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '',
|
||||
ongoingCall: Boolean(call),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
joinCall,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StartCall);
|
||||
75
app/products/calls/components/start_call/start_call.test.js
Normal file
75
app/products/calls/components/start_call/start_call.test.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, TouchableHighlight} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
|
||||
import {shallowWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import StartCall from './start_call';
|
||||
|
||||
describe('StartCall', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
joinCall: jest.fn(),
|
||||
},
|
||||
testID: 'test-id',
|
||||
theme: Preferences.THEMES.denim,
|
||||
currentChannelId: 'channel-id',
|
||||
currentChannelName: 'Channel Name',
|
||||
canStartCall: true,
|
||||
callChannelName: 'Call channel name',
|
||||
confirmToJoin: false,
|
||||
alreadyInTheCall: false,
|
||||
ongoingCall: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(<StartCall {...baseProps}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is already an ongoing call in the channel', () => {
|
||||
const props = {...baseProps, ongoingCall: true};
|
||||
const wrapper = shallowWithIntl(<StartCall {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should be null when you are already in the channel call', () => {
|
||||
const props = {...baseProps, alreadyInTheCall: true};
|
||||
const wrapper = shallowWithIntl(<StartCall {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toBeNull();
|
||||
});
|
||||
|
||||
test('should be null if you can not start a call', () => {
|
||||
const props = {...baseProps, canStartCall: false};
|
||||
const wrapper = shallowWithIntl(<StartCall {...props}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toBeNull();
|
||||
});
|
||||
|
||||
test('should join on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<StartCall {...props}/>).dive();
|
||||
|
||||
wrapper.find(ChannelInfoRow).dive().find(TouchableHighlight).simulate('press');
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should ask for confirmation on click', () => {
|
||||
const joinCall = jest.fn();
|
||||
const props = {...baseProps, confirmToJoin: true, actions: {joinCall}};
|
||||
const wrapper = shallowWithIntl(<StartCall {...props}/>).dive();
|
||||
|
||||
wrapper.find(ChannelInfoRow).dive().find(TouchableHighlight).simulate('press');
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(props.actions.joinCall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
61
app/products/calls/components/start_call/start_call.tsx
Normal file
61
app/products/calls/components/start_call/start_call.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
|
||||
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
|
||||
import Separator from '@screens/channel_info/separator';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
joinCall: (channelId: string) => any;
|
||||
};
|
||||
testID?: string;
|
||||
theme: Theme;
|
||||
currentChannelId: string;
|
||||
currentChannelName: string;
|
||||
callChannelName: string;
|
||||
confirmToJoin: boolean;
|
||||
alreadyInTheCall: boolean;
|
||||
canStartCall: boolean;
|
||||
ongoingCall: boolean;
|
||||
intl: typeof IntlShape;
|
||||
}
|
||||
|
||||
const StartCall = (props: Props) => {
|
||||
const {testID, canStartCall, theme, actions, currentChannelId, callChannelName, currentChannelName, confirmToJoin, alreadyInTheCall, ongoingCall, intl} = props;
|
||||
|
||||
const handleStartCall = useCallback(preventDoubleTap(() => {
|
||||
leaveAndJoinWithAlert(intl, currentChannelId, callChannelName, currentChannelName, confirmToJoin, actions.joinCall);
|
||||
}), [actions.joinCall, currentChannelId]);
|
||||
|
||||
if (!canStartCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (alreadyInTheCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator theme={theme}/>
|
||||
<ChannelInfoRow
|
||||
testID={testID}
|
||||
action={handleStartCall}
|
||||
defaultMessage={ongoingCall ? 'Join Ongoing Call' : 'Start Call'}
|
||||
icon='phone-in-talk'
|
||||
textId={ongoingCall ? t('mobile.channel_info.join_ongoing_call') : t('mobile.channel_info.start_call')}
|
||||
theme={theme}
|
||||
rightArrow={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(StartCall);
|
||||
153
app/products/calls/connection.ts
Normal file
153
app/products/calls/connection.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
import {
|
||||
MediaStream,
|
||||
MediaStreamTrack,
|
||||
mediaDevices,
|
||||
} from 'react-native-webrtc2';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
|
||||
import Peer from './simple-peer';
|
||||
|
||||
export let client: any = null;
|
||||
|
||||
const websocketConnectTimeout = 3000;
|
||||
|
||||
function getWSConnectionURL(channelID: string): string {
|
||||
let url = Client4.getAbsoluteUrl(`/plugins/com.mattermost.calls/${channelID}/ws`);
|
||||
url = url.replace(/^https:/, 'wss:');
|
||||
url = url.replace(/^http:/, 'ws:');
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function newClient(channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
|
||||
let peer: any = null;
|
||||
const streams: MediaStream[] = [];
|
||||
|
||||
let stream: MediaStream;
|
||||
let audioTrack: any;
|
||||
try {
|
||||
stream = await mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: true,
|
||||
}) as MediaStream;
|
||||
audioTrack = stream.getAudioTracks()[0];
|
||||
audioTrack.enabled = false;
|
||||
streams.push(stream);
|
||||
} catch (err) {
|
||||
console.log('Unable to get media device:', err); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
const ws = new WebSocket(getWSConnectionURL(channelID));
|
||||
|
||||
const disconnect = () => {
|
||||
ws.close();
|
||||
|
||||
streams.forEach((s) => {
|
||||
s.getTracks().forEach((track: MediaStreamTrack) => {
|
||||
track.stop();
|
||||
});
|
||||
});
|
||||
|
||||
if (peer) {
|
||||
peer.destroy();
|
||||
}
|
||||
InCallManager.stop();
|
||||
|
||||
if (closeCb) {
|
||||
closeCb();
|
||||
}
|
||||
};
|
||||
|
||||
const mute = () => {
|
||||
if (audioTrack) {
|
||||
audioTrack.enabled = false;
|
||||
}
|
||||
if (ws) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'mute',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const unmute = () => {
|
||||
if (audioTrack) {
|
||||
audioTrack.enabled = true;
|
||||
}
|
||||
if (ws) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'unmute',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => console.log('WS ERROR', err); // eslint-disable-line no-console
|
||||
|
||||
ws.onopen = async () => {
|
||||
InCallManager.start({media: 'audio'});
|
||||
peer = new Peer(stream);
|
||||
peer.on('signal', (data: any) => {
|
||||
if (data.type === 'offer' || data.type === 'answer') {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'signal',
|
||||
data,
|
||||
}));
|
||||
} else if (data.type === 'candidate') {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice',
|
||||
data,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('stream', (remoteStream: MediaStream) => {
|
||||
streams.push(remoteStream);
|
||||
if (remoteStream.getVideoTracks().length > 0) {
|
||||
setScreenShareURL(remoteStream.toURL());
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('error', (err: any) => console.log('PEER ERROR', err)); // eslint-disable-line no-console
|
||||
|
||||
ws.onmessage = ({data}) => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.type === 'answer' || msg.type === 'offer') {
|
||||
peer.signal(data);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const waitForReady = () => {
|
||||
const waitForReadyImpl = (callback: () => void, fail: () => void, timeout: number) => {
|
||||
if (timeout <= 0) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
callback();
|
||||
} else {
|
||||
waitForReadyImpl(callback, fail, timeout - 10);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
waitForReadyImpl(resolve, reject, websocketConnectTimeout);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
client = {
|
||||
disconnect,
|
||||
mute,
|
||||
unmute,
|
||||
waitForReady,
|
||||
};
|
||||
|
||||
return client;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
168
app/products/calls/screens/call/call_screen.test.js
Normal file
168
app/products/calls/screens/call/call_screen.test.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import CallScreen from './call_screen';
|
||||
|
||||
describe('CallScreen', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
muteMyself: jest.fn(),
|
||||
unmuteMyself: jest.fn(),
|
||||
leaveCall: jest.fn(),
|
||||
},
|
||||
theme: Preferences.THEMES.denim,
|
||||
call: {
|
||||
participants: {
|
||||
'user-1-id': {
|
||||
id: 'user-1-id',
|
||||
muted: false,
|
||||
isTalking: false,
|
||||
},
|
||||
'user-2-id': {
|
||||
id: 'user-2-id',
|
||||
muted: true,
|
||||
isTalking: true,
|
||||
},
|
||||
},
|
||||
channelId: 'channel-id',
|
||||
startTime: 100,
|
||||
speakers: 'user-2-id',
|
||||
screenOn: false,
|
||||
threadId: false,
|
||||
},
|
||||
users: {
|
||||
'user-1-id': {
|
||||
id: 'user-1-id',
|
||||
username: 'user-1-username',
|
||||
nickname: 'User 1',
|
||||
},
|
||||
'user-2-id': {
|
||||
id: 'user-2-id',
|
||||
username: 'user-2-username',
|
||||
nickname: 'User 2',
|
||||
},
|
||||
},
|
||||
currentParticipant: {
|
||||
id: 'user-2-id',
|
||||
muted: true,
|
||||
isTalking: true,
|
||||
},
|
||||
teammateNameDisplay: Preferences.DISPLAY_PREFER_NICKNAME,
|
||||
screenShareURL: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.doMock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
|
||||
default: jest.fn().mockReturnValue({width: 800, height: 400}),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('should show controls in landscape view on click the users list', () => {
|
||||
const props = {...baseProps, call: {...baseProps.call, screenOn: false}};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
wrapper.find({testID: 'users-list'}).simulate('press');
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show controls in landscape view on click the screen share', () => {
|
||||
const props = {...baseProps, call: {...baseProps.call, screenOn: true}, screenShareURL: 'screen-share-url'};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
wrapper.find({testID: 'screen-share-container'}).simulate('press');
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
['Portrait', 'Landscape'].forEach((orientation) => {
|
||||
describe(orientation, () => {
|
||||
beforeEach(() => {
|
||||
if (orientation === 'Landscape') {
|
||||
jest.doMock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
|
||||
default: jest.fn().mockReturnValue({width: 800, height: 400}),
|
||||
}));
|
||||
} else {
|
||||
jest.doMock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
|
||||
default: jest.fn().mockReturnValue({width: 400, height: 800}),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<CallScreen {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with screenshare', () => {
|
||||
const props = {...baseProps, call: {...baseProps.call, screenOn: true}, screenShareURL: 'screen-share-url'};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should leave on click leave button', () => {
|
||||
const leaveCall = jest.fn();
|
||||
const props = {...baseProps, actions: {...baseProps.actions, leaveCall}};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'leave'}).simulate('press');
|
||||
expect(props.actions.leaveCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should mute myself on click mute/unmute button if i am not muted', () => {
|
||||
const muteMyself = jest.fn();
|
||||
const unmuteMyself = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
muteMyself,
|
||||
unmuteMyself,
|
||||
},
|
||||
currentParticipant: {
|
||||
...baseProps.currentParticipant,
|
||||
muted: false,
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'mute-unmute'}).simulate('press');
|
||||
expect(props.actions.muteMyself).toHaveBeenCalled();
|
||||
expect(props.actions.unmuteMyself).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should mute myself on click mute/unmute button if i am muted', () => {
|
||||
const muteMyself = jest.fn();
|
||||
const unmuteMyself = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
muteMyself,
|
||||
unmuteMyself,
|
||||
},
|
||||
currentParticipant: {
|
||||
...baseProps.currentParticipant,
|
||||
muted: true,
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'mute-unmute'}).simulate('press');
|
||||
expect(props.actions.muteMyself).not.toHaveBeenCalled();
|
||||
expect(props.actions.unmuteMyself).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
461
app/products/calls/screens/call/call_screen.tsx
Normal file
461
app/products/calls/screens/call/call_screen.tsx
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useCallback, useState} from 'react';
|
||||
import {Keyboard, View, Text, Platform, Pressable, SafeAreaView, ScrollView, useWindowDimensions} from 'react-native';
|
||||
import {RTCView} from 'react-native-webrtc2';
|
||||
|
||||
import {showModalOverCurrentContext, mergeNavigationOptions, popTopScreen, goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import CallAvatar from '@mmproducts/calls/components/call_avatar';
|
||||
import CallDuration from '@mmproducts/calls/components/call_duration';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
import type {IDMappedObjects} from '@mm-redux/types/utilities';
|
||||
import type {Call, CallParticipant} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
muteMyself: (channelId: string) => GenericAction;
|
||||
unmuteMyself: (channelId: string) => GenericAction;
|
||||
leaveCall: () => GenericAction;
|
||||
};
|
||||
theme: Theme;
|
||||
call: Call|null;
|
||||
users: IDMappedObjects<UserProfile>;
|
||||
currentParticipant: CallParticipant;
|
||||
teammateNameDisplay: string;
|
||||
screenShareURL: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((props: any) => {
|
||||
const showControls = !props.isLandscape || props.showControlsInLandscape;
|
||||
const buttons: any = {
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
width: '100%',
|
||||
paddingBottom: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 4,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 4,
|
||||
},
|
||||
}),
|
||||
};
|
||||
if (props.isLandscape) {
|
||||
buttons.height = 128;
|
||||
buttons.position = 'absolute';
|
||||
buttons.backgroundColor = 'rgba(0,0,0,0.64)';
|
||||
buttons.bottom = 0;
|
||||
if (!showControls) {
|
||||
buttons.bottom = 1000;
|
||||
}
|
||||
}
|
||||
const header: any = {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
padding: 14,
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 4,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 4,
|
||||
},
|
||||
}),
|
||||
};
|
||||
if (props.isLandscape) {
|
||||
header.position = 'absolute';
|
||||
header.top = 0;
|
||||
header.backgroundColor = 'rgba(0,0,0,0.64)';
|
||||
header.height = 64;
|
||||
header.padding = 0;
|
||||
if (!showControls) {
|
||||
header.top = -1000;
|
||||
}
|
||||
}
|
||||
const usersScroll: any = {};
|
||||
const users: any = {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
if (props.isLandscape && props.call?.screenOn) {
|
||||
usersScroll.position = 'absolute';
|
||||
usersScroll.height = 0;
|
||||
}
|
||||
return {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
elevation: 3,
|
||||
},
|
||||
ios: {
|
||||
zIndex: 3,
|
||||
},
|
||||
}),
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'black',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 5,
|
||||
alignItems: 'center',
|
||||
},
|
||||
header,
|
||||
time: {
|
||||
flex: 1,
|
||||
color: props.theme.sidebarText,
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
},
|
||||
users,
|
||||
usersScroll,
|
||||
user: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginTop: props.call?.screenOn ? 0 : 10,
|
||||
marginBottom: props.call?.screenOn ? 0 : 10,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
username: {
|
||||
color: props.theme.sidebarText,
|
||||
},
|
||||
buttons,
|
||||
button: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
mute: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: 30,
|
||||
backgroundColor: props.currentParticipant?.muted ? 'rgba(255,255,255,0.16)' : '#3DB887',
|
||||
borderRadius: 20,
|
||||
marginBottom: 10,
|
||||
marginTop: 20,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
otherButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignContent: 'space-between',
|
||||
},
|
||||
collapseIcon: {
|
||||
color: props.theme.sidebarText,
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
muteIcon: {
|
||||
color: props.theme.sidebarText,
|
||||
},
|
||||
buttonText: {
|
||||
color: props.theme.sidebarText,
|
||||
},
|
||||
buttonIcon: {
|
||||
color: props.theme.sidebarText,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
borderRadius: 34,
|
||||
padding: 22,
|
||||
width: 68,
|
||||
height: 68,
|
||||
margin: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
muteIconLandscape: {
|
||||
backgroundColor: props.currentParticipant?.muted ? 'rgba(255,255,255,0.16)' : '#3DB887',
|
||||
},
|
||||
hangUpIcon: {
|
||||
backgroundColor: '#D24B4E',
|
||||
},
|
||||
screenShareImage: {
|
||||
flex: 7,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
screenShareText: {
|
||||
color: 'white',
|
||||
margin: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CallScreen = (props: Props) => {
|
||||
if (!props.call) {
|
||||
return null;
|
||||
}
|
||||
const {width, height} = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
|
||||
const style = getStyleSheet({...props, showControlsInLandscape, isLandscape});
|
||||
useEffect(() => {
|
||||
mergeNavigationOptions('Call', {
|
||||
layout: {
|
||||
componentBackgroundColor: 'black',
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showOtherActions = () => {
|
||||
const screen = 'CallOtherActions';
|
||||
const passProps = {
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
const otherActionsRequest = requestAnimationFrame(() => {
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
cancelAnimationFrame(otherActionsRequest);
|
||||
});
|
||||
};
|
||||
|
||||
const minimizeCallHandler = useCallback(() => popTopScreen(), []);
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
popTopScreen();
|
||||
props.actions.leaveCall();
|
||||
}, [props.actions.leaveCall]);
|
||||
|
||||
const openThreadHandler = useCallback(() => {
|
||||
const passProps = {
|
||||
channelId: props.call?.channelId,
|
||||
rootId: props.call?.threadId,
|
||||
};
|
||||
goToScreen(THREAD, '', passProps);
|
||||
}, [props.call]);
|
||||
|
||||
const muteUnmuteHandler = useCallback(() => {
|
||||
if (props.call) {
|
||||
if (props.currentParticipant?.muted) {
|
||||
props.actions.unmuteMyself(props.call.channelId);
|
||||
} else {
|
||||
props.actions.muteMyself(props.call.channelId);
|
||||
}
|
||||
}
|
||||
}, [props.call.channelId, props.currentParticipant]);
|
||||
|
||||
const toggleControlsInLandscape = useCallback(() => {
|
||||
setShowControlsInLandscape(!showControlsInLandscape);
|
||||
}, [showControlsInLandscape]);
|
||||
|
||||
let screenShareView = null;
|
||||
if (props.screenShareURL && props.call.screenOn) {
|
||||
screenShareView = (
|
||||
<Pressable
|
||||
testID='screen-share-container'
|
||||
style={style.screenShareImage}
|
||||
onPress={toggleControlsInLandscape}
|
||||
>
|
||||
<RTCView
|
||||
streamURL={props.screenShareURL}
|
||||
style={style.screenShareImage}
|
||||
/>
|
||||
<FormattedText
|
||||
id='call.screen_share_user'
|
||||
defaultMessage='You are seing {userDisplayName} screen'
|
||||
values={{userDisplayName: displayUsername(props.users[props.call.screenOn], props.teammateNameDisplay)}}
|
||||
style={style.screenShareText}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
let usersList = null;
|
||||
if (!props.call.screenOn || !isLandscape) {
|
||||
usersList = (
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
horizontal={props.call?.screenOn !== ''}
|
||||
contentContainerStyle={style.usersScroll}
|
||||
>
|
||||
<Pressable
|
||||
testID='users-list'
|
||||
onPress={toggleControlsInLandscape}
|
||||
style={style.users}
|
||||
>
|
||||
{Object.values(props.call.participants).map((user) => {
|
||||
return (
|
||||
<View
|
||||
style={style.user}
|
||||
key={user.id}
|
||||
>
|
||||
<CallAvatar
|
||||
userId={user.id}
|
||||
volume={user.isTalking ? 1 : 0}
|
||||
muted={user.muted}
|
||||
size={props.call?.screenOn ? 'm' : 'l'}
|
||||
/>
|
||||
<Text style={style.username}>{displayUsername(props.users[user.id], props.teammateNameDisplay)}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<View style={style.header}>
|
||||
<CallDuration
|
||||
style={style.time}
|
||||
value={props.call.startTime}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={minimizeCallHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-collapse'
|
||||
size={24}
|
||||
style={style.collapseIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{usersList}
|
||||
{screenShareView}
|
||||
<View style={style.buttons}>
|
||||
{!isLandscape &&
|
||||
<Pressable
|
||||
testID='mute-unmute'
|
||||
style={style.mute}
|
||||
onPress={muteUnmuteHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name={props.currentParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={style.muteIcon}
|
||||
/>
|
||||
{props.currentParticipant?.muted &&
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.unmute'
|
||||
defaultMessage='Unmute'
|
||||
/>}
|
||||
{!props.currentParticipant?.muted &&
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.mute'
|
||||
defaultMessage='Mute'
|
||||
/>}
|
||||
</Pressable>}
|
||||
<View style={style.otherButtons}>
|
||||
<Pressable
|
||||
testID='leave'
|
||||
style={style.button}
|
||||
onPress={leaveCallHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-hangup'
|
||||
size={24}
|
||||
style={{...style.buttonIcon, ...style.hangUpIcon}}
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.leave'
|
||||
defaultMessage='Leave'
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
onPress={openThreadHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='message-text-outline'
|
||||
size={24}
|
||||
style={style.buttonIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.chat_thread'
|
||||
defaultMessage='Chat thread'
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
>
|
||||
<CompassIcon
|
||||
name='settings-outline'
|
||||
size={24}
|
||||
style={style.buttonIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.settings'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
onPress={showOtherActions}
|
||||
>
|
||||
<CompassIcon
|
||||
name='dots-horizontal'
|
||||
size={24}
|
||||
style={style.buttonIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.more'
|
||||
defaultMessage='More'
|
||||
/>
|
||||
</Pressable>
|
||||
{isLandscape &&
|
||||
<Pressable
|
||||
testID='mute-unmute'
|
||||
style={style.button}
|
||||
onPress={muteUnmuteHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name={props.currentParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={{...style.buttonIcon, ...style.muteIconLandscape}}
|
||||
/>
|
||||
{props.currentParticipant?.muted &&
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.unmute'
|
||||
defaultMessage='Unmute'
|
||||
/>}
|
||||
{!props.currentParticipant?.muted &&
|
||||
<FormattedText
|
||||
style={style.buttonText}
|
||||
id='call.mute'
|
||||
defaultMessage='Mute'
|
||||
/>}
|
||||
</Pressable>}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallScreen;
|
||||
38
app/products/calls/screens/call/index.ts
Normal file
38
app/products/calls/screens/call/index.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {muteMyself, unmuteMyself, leaveCall} from '@mmproducts/calls//store/actions/calls';
|
||||
import {getCurrentCall, getScreenShareURL} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import CallScreen from './call_screen';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentCall = getCurrentCall(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
call: currentCall,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
users: state.entities.users.profiles,
|
||||
currentParticipant: currentCall && currentCall.participants[currentUserId],
|
||||
screenShareURL: getScreenShareURL(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
muteMyself,
|
||||
unmuteMyself,
|
||||
leaveCall,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CallScreen);
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Action should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 51,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="action"
|
||||
underlayColor="rgba(0, 0, 0, 0.1)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"width": 60,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="test-icon"
|
||||
size={24}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"letterSpacing": -0.45,
|
||||
"lineHeight": 19,
|
||||
"opacity": 0.9,
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
test-text
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderBottomColor": "rgba(63,67,80,0.2)",
|
||||
"borderBottomWidth": 0.5,
|
||||
"marginHorizontal": 17,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`Action should match snapshot when is destructive 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 51,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="action"
|
||||
underlayColor="rgba(0, 0, 0, 0.1)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"width": 60,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="test-icon"
|
||||
size={24}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
},
|
||||
Object {
|
||||
"color": "#D0021B",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3f4350",
|
||||
"fontSize": 16,
|
||||
"letterSpacing": -0.45,
|
||||
"lineHeight": 19,
|
||||
"opacity": 0.9,
|
||||
},
|
||||
Object {
|
||||
"color": "#D0021B",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test-text
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderBottomColor": "rgba(63,67,80,0.2)",
|
||||
"borderBottomWidth": 0.5,
|
||||
"marginHorizontal": 17,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallOtherActions should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(SlideUpPanel)
|
||||
initialPosition={0.24}
|
||||
onRequestClose={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon="account-plus-outline"
|
||||
onPress={[Function]}
|
||||
text="Add participants"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon="link-variant"
|
||||
onPress={[Function]}
|
||||
text="Copy call link"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon="send-outline"
|
||||
onPress={[Function]}
|
||||
text="Give Feedback"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Connect(SlideUpPanel)>
|
||||
</View>
|
||||
`;
|
||||
42
app/products/calls/screens/call_other_actions/action.test.js
Normal file
42
app/products/calls/screens/call_other_actions/action.test.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import Action from './action';
|
||||
|
||||
describe('Action', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.denim,
|
||||
destructive: false,
|
||||
icon: 'test-icon',
|
||||
onPress: jest.fn(),
|
||||
text: 'test-text',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<Action {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when is destructive', () => {
|
||||
const props = {...baseProps, destructive: true};
|
||||
const wrapper = shallow(<Action {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call on callback on press', () => {
|
||||
const onPress = jest.fn();
|
||||
const props = {...baseProps, onPress};
|
||||
const wrapper = shallow(<Action {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'action'}).simulate('press');
|
||||
|
||||
expect(onPress).toBeCalled();
|
||||
});
|
||||
});
|
||||
129
app/products/calls/screens/call_other_actions/action.tsx
Normal file
129
app/products/calls/screens/call_other_actions/action.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Text,
|
||||
Platform,
|
||||
TouchableHighlight,
|
||||
TouchableNativeFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
|
||||
type Props = {
|
||||
destructive: boolean;
|
||||
icon: string;
|
||||
onPress: () => null;
|
||||
text: string;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const Action = (props: Props) => {
|
||||
const handleOnPress = preventDoubleTap(() => {
|
||||
props.onPress();
|
||||
}, 500);
|
||||
|
||||
const {destructive, icon, text, theme} = props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const Touchable = Platform.select({
|
||||
ios: TouchableHighlight as any,
|
||||
android: TouchableNativeFeedback as any,
|
||||
});
|
||||
|
||||
const touchableProps = Platform.select({
|
||||
ios: {
|
||||
underlayColor: 'rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
android: {
|
||||
background: TouchableNativeFeedback.Ripple( //eslint-disable-line new-cap
|
||||
'rgba(0, 0, 0, 0.1)',
|
||||
false,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.container}
|
||||
>
|
||||
<Touchable
|
||||
testID='action'
|
||||
onPress={handleOnPress}
|
||||
{...touchableProps}
|
||||
style={style.row}
|
||||
>
|
||||
<View style={style.row}>
|
||||
<View style={style.iconContainer}>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
size={24}
|
||||
style={[style.icon, destructive ? style.destructive : null]}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.textContainer}>
|
||||
<Text style={[style.text, destructive ? style.destructive : null]}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Touchable>
|
||||
<View style={style.footer}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 51,
|
||||
width: '100%',
|
||||
},
|
||||
destructive: {
|
||||
color: '#D0021B',
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
width: 60,
|
||||
},
|
||||
noIconContainer: {
|
||||
height: 50,
|
||||
width: 18,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
textContainer: {
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
height: 50,
|
||||
marginRight: 5,
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
lineHeight: 19,
|
||||
opacity: 0.9,
|
||||
letterSpacing: -0.45,
|
||||
},
|
||||
footer: {
|
||||
marginHorizontal: 17,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default Action;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {shallowWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import CallOtherActions from './call_other_actions';
|
||||
|
||||
describe('CallOtherActions', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.denim,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(<CallOtherActions {...baseProps}/>).dive();
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {dismissModal} from '@actions/navigation';
|
||||
import SlideUpPanel from '@components/slide_up_panel';
|
||||
|
||||
import Action from './action';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
intl: typeof intlShape;
|
||||
}
|
||||
|
||||
const CallOtherActions = ({theme, intl}: Props) => {
|
||||
const close = () => {
|
||||
dismissModal();
|
||||
};
|
||||
|
||||
// TODO: Implement this whenever we support participants invitation to calls
|
||||
const addParticipants = useCallback(() => null, []);
|
||||
|
||||
// TODO: Implement this whenever we support calls links
|
||||
const copyCallLink = useCallback(() => null, []);
|
||||
|
||||
// TODO: Implement this whenever we support give feedback
|
||||
const giveFeedback = useCallback(() => null, []);
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<SlideUpPanel
|
||||
onRequestClose={close}
|
||||
initialPosition={0.24}
|
||||
theme={theme}
|
||||
>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon='account-plus-outline'
|
||||
onPress={addParticipants}
|
||||
text={intl.formatMessage({id: 'call.add_participants', defaultMessage: 'Add participants'})}
|
||||
theme={theme}
|
||||
/>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon='link-variant'
|
||||
onPress={copyCallLink}
|
||||
text={intl.formatMessage({id: 'call.copy_call_link', defaultMessage: 'Copy call link'})}
|
||||
theme={theme}
|
||||
/>
|
||||
<Action
|
||||
destructive={false}
|
||||
icon='send-outline'
|
||||
onPress={giveFeedback}
|
||||
text={intl.formatMessage({id: 'call.give_feedback', defaultMessage: 'Give Feedback'})}
|
||||
theme={theme}
|
||||
/>
|
||||
</SlideUpPanel>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(CallOtherActions);
|
||||
18
app/products/calls/screens/call_other_actions/index.ts
Normal file
18
app/products/calls/screens/call_other_actions/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import CallOtherActions from './call_other_actions';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CallOtherActions);
|
||||
1001
app/products/calls/simple-peer.ts
Normal file
1001
app/products/calls/simple-peer.ts
Normal file
File diff suppressed because it is too large
Load diff
25
app/products/calls/store/action_types/calls.ts
Normal file
25
app/products/calls/store/action_types/calls.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from '@mm-redux/utils/key_mirror';
|
||||
|
||||
export default keyMirror({
|
||||
RECEIVED_CALLS: null,
|
||||
RECEIVED_CALL_STARTED: null,
|
||||
RECEIVED_CALL_FINISHED: null,
|
||||
RECEIVED_CHANNEL_CALL_ENABLED: null,
|
||||
RECEIVED_CHANNEL_CALL_DISABLED: null,
|
||||
RECEIVED_CHANNEL_CALL_SCREEN_ON: null,
|
||||
RECEIVED_CHANNEL_CALL_SCREEN_OFF: null,
|
||||
RECEIVED_JOINED_CALL: null,
|
||||
RECEIVED_LEFT_CALL: null,
|
||||
RECEIVED_MYSELF_JOINED_CALL: null,
|
||||
RECEIVED_MYSELF_LEFT_CALL: null,
|
||||
RECEIVED_MUTE_USER_CALL: null,
|
||||
RECEIVED_UNMUTE_USER_CALL: null,
|
||||
RECEIVED_VOICE_ON_USER_CALL: null,
|
||||
RECEIVED_VOICE_OFF_USER_CALL: null,
|
||||
RECEIVED_RAISE_HAND_CALL: null,
|
||||
RECEIVED_UNRAISE_HAND_CALL: null,
|
||||
SET_SCREENSHARE_URL: null,
|
||||
});
|
||||
144
app/products/calls/store/actions/calls.test.js
Normal file
144
app/products/calls/store/actions/calls.test.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import configureStore from '@test/test_store';
|
||||
|
||||
import CallsTypes from '../action_types/calls';
|
||||
|
||||
import * as CallsActions from './calls';
|
||||
|
||||
jest.mock('@client/rest', () => ({
|
||||
Client4: {
|
||||
setUrl: jest.fn(),
|
||||
getCalls: jest.fn(() => [
|
||||
{
|
||||
call: {
|
||||
users: ['user-1', 'user-2'],
|
||||
states: {
|
||||
'user-1': {unmuted: true},
|
||||
'user-2': {unmuted: false},
|
||||
},
|
||||
start_at: 123,
|
||||
screen_sharing_id: '',
|
||||
thread_id: 'thread-1',
|
||||
},
|
||||
channel_id: 'channel-1',
|
||||
enabled: true,
|
||||
},
|
||||
]),
|
||||
enableChannelCalls: jest.fn(() => null),
|
||||
disableChannelCalls: jest.fn(() => null),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@mmproducts/calls/connection', () => ({
|
||||
newClient: jest.fn(() => Promise.resolve({
|
||||
disconnect: jest.fn(),
|
||||
mute: jest.fn(),
|
||||
unmute: jest.fn(),
|
||||
waitForReady: jest.fn(() => Promise.resolve()),
|
||||
})),
|
||||
}));
|
||||
|
||||
export function addFakeCall(channelId) {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CALL_STARTED,
|
||||
data: {
|
||||
participants: {
|
||||
xohi8cki9787fgiryne716u84o: {id: 'xohi8cki9787fgiryne716u84o', isTalking: true, muted: false},
|
||||
xohi8cki9787fgiryne716u841: {id: 'xohi8cki9787fgiryne716u84o', isTalking: true, muted: true},
|
||||
xohi8cki9787fgiryne716u842: {id: 'xohi8cki9787fgiryne716u84o', isTalking: false, uted: false},
|
||||
xohi8cki9787fgiryne716u843: {id: 'xohi8cki9787fgiryne716u84o', isTalking: false, muted: true},
|
||||
xohi8cki9787fgiryne716u844: {id: 'xohi8cki9787fgiryne716u84o', isTalking: true, muted: false},
|
||||
xohi8cki9787fgiryne716u845: {id: 'xohi8cki9787fgiryne716u84o', isTalking: true, muted: true},
|
||||
},
|
||||
channelId,
|
||||
startTime: (new Date()).getTime(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Actions.Calls', () => {
|
||||
let store;
|
||||
const {newClient} = require('@mmproducts/calls/connection');
|
||||
|
||||
beforeEach(async () => {
|
||||
newClient.mockClear();
|
||||
Client4.setUrl.mockClear();
|
||||
Client4.getCalls.mockClear();
|
||||
Client4.enableChannelCalls.mockClear();
|
||||
Client4.disableChannelCalls.mockClear();
|
||||
store = await configureStore();
|
||||
});
|
||||
|
||||
it('joinCall', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
const response = await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
const result = store.getState().entities.calls.joined;
|
||||
assert.equal('channel-id', result);
|
||||
assert.equal(response.data, 'channel-id');
|
||||
expect(newClient).toBeCalled();
|
||||
expect(newClient.mock.calls[0][0]).toBe('channel-id');
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
});
|
||||
|
||||
it('leaveCall', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
expect(CallsActions.ws).toBe(null);
|
||||
|
||||
await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
let result = store.getState().entities.calls.joined;
|
||||
assert.equal('channel-id', result);
|
||||
|
||||
expect(CallsActions.ws.disconnect).not.toBeCalled();
|
||||
const disconnectMock = CallsActions.ws.disconnect;
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
expect(disconnectMock).toBeCalled();
|
||||
expect(CallsActions.ws).toBe(null);
|
||||
|
||||
result = store.getState().entities.calls.joined;
|
||||
assert.equal('', result);
|
||||
});
|
||||
|
||||
it('muteMyself', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
await store.dispatch(CallsActions.muteMyself());
|
||||
expect(CallsActions.ws.mute).toBeCalled();
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
});
|
||||
|
||||
it('unmuteMyself', async () => {
|
||||
await store.dispatch(addFakeCall('channel-id'));
|
||||
await store.dispatch(CallsActions.joinCall('channel-id'));
|
||||
await store.dispatch(CallsActions.unmuteMyself());
|
||||
expect(CallsActions.ws.unmute).toBeCalled();
|
||||
await store.dispatch(CallsActions.leaveCall());
|
||||
});
|
||||
|
||||
it('loadCalls', async () => {
|
||||
await store.dispatch(CallsActions.loadCalls());
|
||||
expect(Client4.getCalls).toBeCalledWith();
|
||||
assert.equal(store.getState().entities.calls.calls['channel-1'].channelId, 'channel-1');
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], true);
|
||||
});
|
||||
|
||||
it('enableChannelCalls', async () => {
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], undefined);
|
||||
await store.dispatch(CallsActions.enableChannelCalls('channel-1'));
|
||||
expect(Client4.enableChannelCalls).toBeCalledWith('channel-1');
|
||||
});
|
||||
|
||||
it('disableChannelCalls', async () => {
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], undefined);
|
||||
await store.dispatch(CallsActions.enableChannelCalls('channel-1'));
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], true);
|
||||
expect(Client4.disableChannelCalls).not.toBeCalledWith('channel-1');
|
||||
await store.dispatch(CallsActions.disableChannelCalls('channel-1'));
|
||||
expect(Client4.disableChannelCalls).toBeCalledWith('channel-1');
|
||||
assert.equal(store.getState().entities.calls.enabled['channel-1'], false);
|
||||
});
|
||||
});
|
||||
150
app/products/calls/store/actions/calls.ts
Normal file
150
app/products/calls/store/actions/calls.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers';
|
||||
import {GenericAction, ActionFunc, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import {newClient} from '@mmproducts/calls/connection';
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
|
||||
import type {Call, CallParticipant} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
export let ws: any = null;
|
||||
|
||||
export function loadCalls(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
let resp = [];
|
||||
try {
|
||||
resp = await Client4.getCalls();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
const callsResults: Dictionary<Call> = {};
|
||||
const enabledChannels: Dictionary<boolean> = {};
|
||||
for (let i = 0; i < resp.length; i++) {
|
||||
const channel = resp[i];
|
||||
if (channel.call) {
|
||||
callsResults[channel.channel_id] = {
|
||||
participants: channel.call.users.reduce((prev: Dictionary<CallParticipant>, cur: string, curIdx: number) => {
|
||||
const muted = channel.call.states && channel.call.states[curIdx] ? !channel.call.states[curIdx].unmuted : true;
|
||||
prev[cur] = {id: cur, muted, isTalking: false};
|
||||
return prev;
|
||||
}, {}),
|
||||
channelId: channel.channel_id,
|
||||
startTime: channel.call.start_at,
|
||||
speakers: [],
|
||||
screenOn: channel.call.screen_sharing_id,
|
||||
threadId: channel.call.thread_id,
|
||||
};
|
||||
}
|
||||
enabledChannels[channel.channel_id] = channel.enabled;
|
||||
}
|
||||
|
||||
const data = {
|
||||
calls: callsResults,
|
||||
enabled: enabledChannels,
|
||||
};
|
||||
|
||||
dispatch({type: CallsTypes.RECEIVED_CALLS, data});
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function enableChannelCalls(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
try {
|
||||
await Client4.enableChannelCalls(channelId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({type: CallsTypes.RECEIVED_CHANNEL_CALL_ENABLED, data: channelId});
|
||||
|
||||
return {data: channelId};
|
||||
};
|
||||
}
|
||||
|
||||
export function disableChannelCalls(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
try {
|
||||
await Client4.disableChannelCalls(channelId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({type: CallsTypes.RECEIVED_CHANNEL_CALL_DISABLED, data: channelId});
|
||||
|
||||
return {data: channelId};
|
||||
};
|
||||
}
|
||||
|
||||
export function joinCall(channelId: string): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const setScreenShareURL = (url: string) => {
|
||||
dispatch({
|
||||
type: CallsTypes.SET_SCREENSHARE_URL,
|
||||
data: url,
|
||||
});
|
||||
};
|
||||
|
||||
if (ws) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = await newClient(channelId, () => null, setScreenShareURL);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
await ws.waitForReady();
|
||||
dispatch({
|
||||
type: CallsTypes.RECEIVED_MYSELF_JOINED_CALL,
|
||||
data: channelId,
|
||||
});
|
||||
return {data: channelId};
|
||||
} catch (e) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
return {error: 'unable to connect to the voice call'};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveCall(): GenericAction {
|
||||
if (ws) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL,
|
||||
};
|
||||
}
|
||||
|
||||
export function muteMyself(): GenericAction {
|
||||
if (ws) {
|
||||
ws.mute();
|
||||
}
|
||||
return {type: 'empty'};
|
||||
}
|
||||
|
||||
export function unmuteMyself(): GenericAction {
|
||||
if (ws) {
|
||||
ws.unmute();
|
||||
}
|
||||
return {type: 'empty'};
|
||||
}
|
||||
83
app/products/calls/store/actions/websockets.ts
Normal file
83
app/products/calls/store/actions/websockets.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
|
||||
export function handleCallUserDisconnected(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_LEFT_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallUserConnected(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_JOINED_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallUserMuted(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_MUTE_USER_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallUserUnmuted(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_UNMUTE_USER_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallUserVoiceOn(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_VOICE_ON_USER_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallUserVoiceOff(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_VOICE_OFF_USER_CALL,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallStarted(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CALL_STARTED,
|
||||
data: {channelId: msg.data.channelID, startTime: msg.data.start_at, threadId: msg.data.thread_id, participants: {}},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallChannelEnabled(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_ENABLED,
|
||||
data: msg.broadcast.channel_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallChannelDisabled(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_DISABLED,
|
||||
data: msg.broadcast.channel_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallScreenOn(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_ON,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
|
||||
export function handleCallScreenOff(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_OFF,
|
||||
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
|
||||
};
|
||||
}
|
||||
362
app/products/calls/store/reducers/calls.test.js
Normal file
362
app/products/calls/store/reducers/calls.test.js
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
|
||||
import callsReducer from './calls';
|
||||
|
||||
describe('Reducers.calls.calls', () => {
|
||||
const call1 = {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, isTalking: false},
|
||||
'user-2': {id: 'user-2', muted: true, isTalking: true},
|
||||
},
|
||||
channelId: 'channel-1',
|
||||
startTime: 123,
|
||||
speakers: ['user-2'],
|
||||
screenOn: '',
|
||||
threadId: 'thread-1',
|
||||
};
|
||||
const call2 = {
|
||||
participants: {
|
||||
'user-3': {id: 'user-3', muted: false, isTalking: false},
|
||||
'user-4': {id: 'user-4', muted: true, isTalking: true},
|
||||
},
|
||||
channelId: 'channel-2',
|
||||
startTime: 123,
|
||||
speakers: ['user-4'],
|
||||
screenOn: '',
|
||||
threadId: 'thread-2',
|
||||
};
|
||||
const call3 = {
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, isTalking: false},
|
||||
'user-6': {id: 'user-6', muted: true, isTalking: true},
|
||||
},
|
||||
channelId: 'channel-3',
|
||||
startTime: 123,
|
||||
speakers: ['user-6'],
|
||||
screenOn: '',
|
||||
threadId: 'thread-3',
|
||||
};
|
||||
it('initial state', async () => {
|
||||
let state = {};
|
||||
|
||||
state = callsReducer(state, {});
|
||||
assert.deepEqual(state.calls, {}, 'initial state');
|
||||
});
|
||||
|
||||
it('RECEIVED_CALLS', async () => {
|
||||
let state = {calls: {calls: {'channel-1': call1}}, enabled: {'channel-1': true}, joined: 'channel-1'};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALLS,
|
||||
data: {calls: {'channel-1': call2, 'channel-2': call3}, enabled: {'channel-1': true}},
|
||||
};
|
||||
|
||||
state = callsReducer(state, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-1': call2, 'channel-2': call3});
|
||||
});
|
||||
|
||||
it('RECEIVED_LEFT_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_LEFT_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-1'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(
|
||||
state.calls,
|
||||
{
|
||||
'channel-1': {
|
||||
participants: {
|
||||
'user-2': {id: 'user-2', muted: true, isTalking: true},
|
||||
},
|
||||
channelId: 'channel-1',
|
||||
startTime: 123,
|
||||
speakers: ['user-2'],
|
||||
screenOn: false,
|
||||
threadId: 'thread-1',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'not-valid-user'};
|
||||
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-1': call1});
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-1'};
|
||||
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-1': call1});
|
||||
});
|
||||
|
||||
it('RECEIVED_JOINED_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_JOINED_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-3'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(
|
||||
state.calls,
|
||||
{
|
||||
'channel-1': {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, isTalking: false},
|
||||
'user-2': {id: 'user-2', muted: true, isTalking: true},
|
||||
'user-3': {id: 'user-3', muted: true, isTalking: false},
|
||||
},
|
||||
channelId: 'channel-1',
|
||||
startTime: 123,
|
||||
speakers: ['user-2'],
|
||||
screenOn: false,
|
||||
threadId: 'thread-1',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-1'};
|
||||
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-1': call1});
|
||||
});
|
||||
|
||||
it('RECEIVED_CALL_STARTED', async () => {
|
||||
const initialState = {calls: {}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALL_STARTED,
|
||||
data: call1,
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-1': call1});
|
||||
});
|
||||
|
||||
it('RECEIVED_CALL_FINISHED', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALL_FINISHED,
|
||||
data: call1,
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, {'channel-2': call2});
|
||||
});
|
||||
|
||||
it('RECEIVED_MUTE_USER_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_MUTE_USER_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-1'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].participants['user-1'].muted, true);
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-1'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
|
||||
it('RECEIVED_UNMUTE_USER_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_UNMUTE_USER_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-2'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].participants['user-2'].muted, false);
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-2'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
|
||||
it('RECEIVED_VOICE_ON_USER_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_VOICE_ON_USER_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-1'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].participants['user-1'].isTalking, true);
|
||||
assert.deepEqual(state.calls['channel-1'].speakers, ['user-1', 'user-2']);
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-2'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
|
||||
it('RECEIVED_VOICE_OFF_USER_CALL', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_VOICE_OFF_USER_CALL,
|
||||
data: {channelId: 'channel-1', userId: 'user-2'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].participants['user-2'].isTalking, false);
|
||||
assert.deepEqual(state.calls['channel-1'].speakers, []);
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-2'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
it('RECEIVED_CHANNEL_CALL_SCREEN_ON', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_ON,
|
||||
data: {channelId: 'channel-1', userId: 'user-1'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].screenOn, 'user-1');
|
||||
|
||||
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel', userId: 'user-1'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
it('RECEIVED_CHANNEL_CALL_SCREEN_OFF', async () => {
|
||||
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_OFF,
|
||||
data: {channelId: 'channel-1'},
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.calls['channel-1'].screenOn, '');
|
||||
|
||||
testAction.data = {channelId: 'invalid-channel'};
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.calls, initialState.calls);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reducers.calls.joined', () => {
|
||||
it('RECEIVED_CALLS', async () => {
|
||||
const initialState = {joined: 'test'};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALLS,
|
||||
data: {calls: {'channel-1': {}, 'channel-2': {}}, enabled: {'channel-1': true}},
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.joined, '');
|
||||
});
|
||||
|
||||
it('RECEIVED_MYSELF_JOINED_CALL', async () => {
|
||||
const initialState = {joined: ''};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_MYSELF_JOINED_CALL,
|
||||
data: 'channel-id',
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.joined, 'channel-id');
|
||||
});
|
||||
|
||||
it('RECEIVED_MYSELF_LEFT_CALL', async () => {
|
||||
const initialState = {joined: 'test'};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL,
|
||||
data: null,
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.equal(state.joined, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reducers.calls.enabled', () => {
|
||||
it('RECEIVED_CALLS', async () => {
|
||||
const initialState = {enabled: {}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CALLS,
|
||||
data: {calls: {'channel-1': {}, 'channel-2': {}}, enabled: {'channel-1': true}},
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true});
|
||||
});
|
||||
|
||||
it('RECEIVED_CHANNEL_CALL_ENABLED', async () => {
|
||||
const initialState = {enabled: {'channel-1': true, 'channel-2': false}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_ENABLED,
|
||||
data: 'channel-3',
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true, 'channel-2': false, 'channel-3': true});
|
||||
|
||||
testAction.data = 'channel-2';
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true, 'channel-2': true});
|
||||
|
||||
testAction.data = 'channel-1';
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true, 'channel-2': false});
|
||||
});
|
||||
|
||||
it('RECEIVED_CHANNEL_CALL_DISABLED', async () => {
|
||||
const initialState = {enabled: {'channel-1': true, 'channel-2': false}};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_CHANNEL_CALL_DISABLED,
|
||||
data: 'channel-3',
|
||||
};
|
||||
let state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true, 'channel-2': false, 'channel-3': false});
|
||||
|
||||
testAction.data = 'channel-2';
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': true, 'channel-2': false});
|
||||
|
||||
testAction.data = 'channel-1';
|
||||
state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.enabled, {'channel-1': false, 'channel-2': false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reducers.calls.screenShareURL', () => {
|
||||
it('RECEIVED_MYSELF_JOINED_CALL', async () => {
|
||||
const initialState = {screenShareURL: 'test'};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_MYSELF_JOINED_CALL,
|
||||
data: 'channel-id',
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.screenShareURL, '');
|
||||
});
|
||||
|
||||
it('RECEIVED_MYSELF_LEFT_CALL', async () => {
|
||||
const initialState = {screenShareURL: 'test'};
|
||||
const testAction = {
|
||||
type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL,
|
||||
data: 'channel-id',
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.screenShareURL, '');
|
||||
});
|
||||
|
||||
it('SET_SCREENSHARE_URL', async () => {
|
||||
const initialState = {screenShareURL: 'test'};
|
||||
const testAction = {
|
||||
type: CallsTypes.SET_SCREENSHARE_URL,
|
||||
data: 'new-url',
|
||||
};
|
||||
const state = callsReducer(initialState, testAction);
|
||||
assert.deepEqual(state.screenShareURL, 'new-url');
|
||||
});
|
||||
});
|
||||
210
app/products/calls/store/reducers/calls.ts
Normal file
210
app/products/calls/store/reducers/calls.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
import {Call} from '@mmproducts/calls/store/types/calls';
|
||||
|
||||
function calls(state: Dictionary<Call> = {}, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.RECEIVED_CALLS: {
|
||||
return action.data.calls;
|
||||
}
|
||||
case CallsTypes.RECEIVED_LEFT_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
delete channelUpdate.participants[userId];
|
||||
const nextState = {...state};
|
||||
if (Object.keys(channelUpdate.participants).length === 0) {
|
||||
delete nextState[channelId];
|
||||
} else {
|
||||
nextState[channelId] = channelUpdate;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_JOINED_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
channelUpdate.participants[userId] = {
|
||||
id: userId,
|
||||
muted: true,
|
||||
isTalking: false,
|
||||
};
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALL_STARTED: {
|
||||
const newCall = action.data;
|
||||
const nextState = {...state};
|
||||
nextState[newCall.channelId] = newCall;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALL_FINISHED: {
|
||||
const newCall = action.data;
|
||||
const nextState = {...state};
|
||||
delete nextState[newCall.channelId];
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_MUTE_USER_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const userUpdate = {...state[channelId].participants[userId], muted: true};
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
channelUpdate.participants[userId] = userUpdate;
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_UNMUTE_USER_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const userUpdate = {...state[channelId].participants[userId], muted: false};
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
channelUpdate.participants[userId] = userUpdate;
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_VOICE_ON_USER_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const userUpdate = {...state[channelId].participants[userId], isTalking: true};
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
channelUpdate.participants[userId] = userUpdate;
|
||||
channelUpdate.speakers = [userId, ...(channelUpdate.speakers || [])];
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_VOICE_OFF_USER_CALL: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const userUpdate = {...state[channelId].participants[userId], isTalking: false};
|
||||
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
|
||||
channelUpdate.participants[userId] = userUpdate;
|
||||
channelUpdate.speakers = channelUpdate.speakers?.filter((id) => id !== userId);
|
||||
if (!channelUpdate.speakers) {
|
||||
channelUpdate.speakers = [];
|
||||
}
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_ON: {
|
||||
const {channelId, userId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
if (!state[channelId].participants[userId]) {
|
||||
return state;
|
||||
}
|
||||
const channelUpdate = {...state[channelId], screenOn: userId};
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_OFF: {
|
||||
const {channelId} = action.data;
|
||||
if (!state[channelId]) {
|
||||
return state;
|
||||
}
|
||||
const channelUpdate = {...state[channelId], screenOn: ''};
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = channelUpdate;
|
||||
return nextState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function joined(state = '', action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.RECEIVED_MYSELF_JOINED_CALL: {
|
||||
return action.data;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CALLS: {
|
||||
return '';
|
||||
}
|
||||
case CallsTypes.RECEIVED_MYSELF_LEFT_CALL: {
|
||||
return '';
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function enabled(state: Dictionary<boolean> = {}, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.RECEIVED_CALLS: {
|
||||
return action.data.enabled;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CHANNEL_CALL_ENABLED: {
|
||||
const nextState = {...state};
|
||||
nextState[action.data] = true;
|
||||
return nextState;
|
||||
}
|
||||
case CallsTypes.RECEIVED_CHANNEL_CALL_DISABLED: {
|
||||
const nextState = {...state};
|
||||
nextState[action.data] = false;
|
||||
return nextState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function screenShareURL(state = '', action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.RECEIVED_MYSELF_JOINED_CALL: {
|
||||
return '';
|
||||
}
|
||||
case CallsTypes.RECEIVED_MYSELF_LEFT_CALL: {
|
||||
return '';
|
||||
}
|
||||
case CallsTypes.SET_SCREENSHARE_URL: {
|
||||
return action.data;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
calls,
|
||||
enabled,
|
||||
joined,
|
||||
screenShareURL,
|
||||
});
|
||||
80
app/products/calls/store/selectors/calls.test.js
Normal file
80
app/products/calls/store/selectors/calls.test.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze';
|
||||
|
||||
import * as Selectors from './calls';
|
||||
|
||||
describe('Selectors.Calls', () => {
|
||||
const call1 = {id: 'call1'};
|
||||
const call2 = {id: 'call2'};
|
||||
const testState = deepFreezeAndThrowOnMutation({
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'channel-1',
|
||||
},
|
||||
calls: {
|
||||
calls: {call1, call2},
|
||||
joined: 'call1',
|
||||
enabled: {'channel-1': true, 'channel-2': false},
|
||||
screenShareURL: 'screenshare-url',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('getCalls', () => {
|
||||
assert.deepEqual(Selectors.getCalls(testState), {call1, call2});
|
||||
});
|
||||
|
||||
it('getCurrentCall', () => {
|
||||
assert.deepEqual(Selectors.getCurrentCall(testState), call1);
|
||||
let newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
joined: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.equal(Selectors.getCurrentCall(newState), null);
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
calls: {
|
||||
...testState.entities.calls,
|
||||
joined: 'invalid-id',
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.equal(Selectors.getCurrentCall(newState), null);
|
||||
});
|
||||
|
||||
it('isCallsEnabled', () => {
|
||||
assert.equal(Selectors.isCallsEnabled(testState), true);
|
||||
let newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
channels: {currentChannelId: 'channel-2'},
|
||||
},
|
||||
};
|
||||
assert.equal(Selectors.isCallsEnabled(newState), false);
|
||||
newState = {
|
||||
...testState,
|
||||
entities: {
|
||||
...testState.entities,
|
||||
channels: {currentChannelId: 'not-valid-channel'},
|
||||
},
|
||||
};
|
||||
assert.equal(Selectors.isCallsEnabled(newState), false);
|
||||
});
|
||||
|
||||
it('getScreenShareURL', () => {
|
||||
assert.equal(Selectors.getScreenShareURL(testState), 'screenshare-url');
|
||||
});
|
||||
});
|
||||
25
app/products/calls/store/selectors/calls.ts
Normal file
25
app/products/calls/store/selectors/calls.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
export function getCalls(state: GlobalState) {
|
||||
return state.entities.calls.calls;
|
||||
}
|
||||
|
||||
export function getCurrentCall(state: GlobalState) {
|
||||
const currentCall = state.entities.calls.joined;
|
||||
if (!currentCall) {
|
||||
return null;
|
||||
}
|
||||
return state.entities.calls.calls[currentCall];
|
||||
}
|
||||
|
||||
export function isCallsEnabled(state: GlobalState) {
|
||||
return Boolean(state.entities.calls.enabled[getCurrentChannelId(state)]);
|
||||
}
|
||||
|
||||
export function getScreenShareURL(state: GlobalState) {
|
||||
return state.entities.calls.screenShareURL;
|
||||
}
|
||||
46
app/products/calls/store/types/calls.ts
Normal file
46
app/products/calls/store/types/calls.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
|
||||
export type CallsState = {
|
||||
calls: Dictionary<Call>;
|
||||
enabled: Dictionary<boolean>;
|
||||
joined: string;
|
||||
screenShareURL: string;
|
||||
}
|
||||
|
||||
export type Call = {
|
||||
participants: Dictionary<CallParticipant>;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
speakers: string[];
|
||||
screenOn: string;
|
||||
threadId: string;
|
||||
}
|
||||
|
||||
export type CallParticipant = {
|
||||
id: string;
|
||||
muted: boolean;
|
||||
isTalking: boolean;
|
||||
}
|
||||
|
||||
export type ServerChannelState = {
|
||||
channel_id: string;
|
||||
enabled: boolean;
|
||||
call: ServerCallState;
|
||||
}
|
||||
|
||||
export type ServerUserState = {
|
||||
unmuted: boolean;
|
||||
raised_hand: number;
|
||||
}
|
||||
|
||||
export type ServerCallState = {
|
||||
id: string;
|
||||
start_at: number;
|
||||
users: string[];
|
||||
states: ServerUserState[];
|
||||
thread_id: string;
|
||||
screen_sharing_id: string;
|
||||
}
|
||||
|
|
@ -14,6 +14,9 @@ import PostDraft from '@components/post_draft';
|
|||
import {NavigationTypes} from '@constants';
|
||||
import {CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE} from '@constants/post_draft';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import CurrentCall from '@mmproducts/calls/components/current_call';
|
||||
import FloatingCallContainer from '@mmproducts/calls/components/floating_call_container';
|
||||
import JoinCall from '@mmproducts/calls/components/join_call';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import ChannelBase from './channel_base';
|
||||
|
|
@ -61,7 +64,7 @@ export default class ChannelAndroid extends ChannelBase {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {theme, viewingGlobalThreads} = this.props;
|
||||
const {theme, viewingGlobalThreads, callsFeatureEnabled} = this.props;
|
||||
let component;
|
||||
|
||||
if (viewingGlobalThreads) {
|
||||
|
|
@ -102,6 +105,11 @@ export default class ChannelAndroid extends ChannelBase {
|
|||
{component}
|
||||
<NetworkIndicator/>
|
||||
<AnnouncementBanner/>
|
||||
{callsFeatureEnabled &&
|
||||
<FloatingCallContainer>
|
||||
<JoinCall/>
|
||||
<CurrentCall/>
|
||||
</FloatingCallContainer>}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import SettingsSidebar from '@components/sidebars/settings';
|
|||
import StatusBar from '@components/status_bar';
|
||||
import DEVICE from '@constants/device';
|
||||
import {ACCESSORIES_CONTAINER_NATIVE_ID, CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE} from '@constants/post_draft';
|
||||
import CurrentCall from '@mmproducts/calls/components/current_call';
|
||||
import FloatingCallContainer from '@mmproducts/calls/components/floating_call_container';
|
||||
import JoinCall from '@mmproducts/calls/components/join_call';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ChannelBase from './channel_base';
|
||||
|
|
@ -54,7 +57,7 @@ export default class ChannelIOS extends ChannelBase {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {currentChannelId, theme, viewingGlobalThreads} = this.props;
|
||||
const {currentChannelId, theme, viewingGlobalThreads, callsFeatureEnabled} = this.props;
|
||||
|
||||
let component;
|
||||
let renderDraftArea = false;
|
||||
|
|
@ -82,6 +85,11 @@ export default class ChannelIOS extends ChannelBase {
|
|||
<>
|
||||
<AnnouncementBanner/>
|
||||
<NetworkIndicator/>
|
||||
{callsFeatureEnabled &&
|
||||
<FloatingCallContainer>
|
||||
<JoinCall/>
|
||||
<CurrentCall/>
|
||||
</FloatingCallContainer>}
|
||||
</>
|
||||
);
|
||||
const header = (
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default class ChannelBase extends PureComponent {
|
|||
loadChannelsForTeam: PropTypes.func.isRequired,
|
||||
selectDefaultTeam: PropTypes.func.isRequired,
|
||||
selectInitialChannel: PropTypes.func.isRequired,
|
||||
loadCalls: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
componentId: PropTypes.string.isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
|
|
@ -38,6 +39,7 @@ export default class ChannelBase extends PureComponent {
|
|||
skipMetrics: PropTypes.bool,
|
||||
viewingGlobalThreads: PropTypes.bool,
|
||||
collapsedThreadsEnabled: PropTypes.bool.isRequired,
|
||||
callsFeatureEnabled: PropTypes.bool.isRequired,
|
||||
selectedPost: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
channel_id: PropTypes.string.isRequired,
|
||||
|
|
@ -102,6 +104,10 @@ export default class ChannelBase extends PureComponent {
|
|||
// Only display the Alert if the TOS does not need to show first
|
||||
unsupportedServer(isSystemAdmin, this.context.intl.formatMessage);
|
||||
}
|
||||
|
||||
if (this.props.callsFeatureEnabled) {
|
||||
this.props.actions.loadCalls();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ describe('ChannelBase', () => {
|
|||
markChannelViewedAndRead: jest.fn(),
|
||||
selectDefaultTeam: jest.fn(),
|
||||
selectInitialChannel: jest.fn(),
|
||||
loadCalls: jest.fn(),
|
||||
},
|
||||
componentId: channelBaseComponentId,
|
||||
theme: Preferences.THEMES.denim,
|
||||
collapsedThreadsEnabled: false,
|
||||
callsFeatureEnabled: false,
|
||||
};
|
||||
const optionsForTheme = (theme) => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ import {Client4} from '@client/rest';
|
|||
import {ViewTypes} from '@constants';
|
||||
import {getChannelStats} from '@mm-redux/actions/channels';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getServerVersion} from '@mm-redux/selectors/entities/general';
|
||||
import {getServerVersion, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getSelectedPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@mm-redux/selectors/entities/users';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils';
|
||||
import {loadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
|
||||
import Channel from './channel';
|
||||
|
|
@ -41,6 +42,7 @@ function mapStateToProps(state) {
|
|||
const currentTeamId = currentTeam?.delete_at === 0 ? currentTeam?.id : '';
|
||||
const currentChannelId = currentTeam?.delete_at === 0 ? getCurrentChannelId(state) : '';
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
|
||||
return {
|
||||
currentChannelId,
|
||||
|
|
@ -54,6 +56,7 @@ function mapStateToProps(state) {
|
|||
teamName: currentTeam?.display_name,
|
||||
theme: getTheme(state),
|
||||
viewingGlobalThreads: collapsedThreadsEnabled && getViewingGlobalThreads(state),
|
||||
callsFeatureEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +67,7 @@ function mapDispatchToProps(dispatch) {
|
|||
loadChannelsForTeam,
|
||||
selectDefaultTeam,
|
||||
selectInitialChannel,
|
||||
loadCalls,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,8 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
|||
|
||||
import {dismissModal} from '@actions/navigation';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import EnableDisableCalls from '@mmproducts/calls/components/enable_disable_calls';
|
||||
import StartCall from '@mmproducts/calls/components/start_call';
|
||||
import {alertErrorWithFallback} from '@utils/general';
|
||||
import {t} from '@utils/i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -38,6 +40,9 @@ export default class ChannelInfo extends PureComponent {
|
|||
getChannelStats: PropTypes.func.isRequired,
|
||||
getCustomEmojisInText: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
joinCall: PropTypes.func.isRequired,
|
||||
enableChannelCalls: PropTypes.func.isRequired,
|
||||
disableChannelCalls: PropTypes.func.isRequired,
|
||||
}),
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
currentChannelCreatorName: PropTypes.string,
|
||||
|
|
@ -52,6 +57,9 @@ export default class ChannelInfo extends PureComponent {
|
|||
isCustomStatusEnabled: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpired: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpirySupported: PropTypes.bool.isRequired,
|
||||
isCallsEnabled: PropTypes.bool.isRequired,
|
||||
isChannelAdmin: PropTypes.bool.isRequired,
|
||||
callsFeatureEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -84,6 +92,19 @@ export default class ChannelInfo extends PureComponent {
|
|||
dismissModal();
|
||||
};
|
||||
|
||||
startCallHandler = (channelId) => {
|
||||
this.props.actions.joinCall(channelId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
toggleCalls = () => {
|
||||
if (this.props.isCallsEnabled) {
|
||||
this.props.actions.disableChannelCalls(this.props.currentChannel.id);
|
||||
} else {
|
||||
this.props.actions.enableChannelCalls(this.props.currentChannel.id);
|
||||
}
|
||||
}
|
||||
|
||||
permalinkBadTeam = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
|
|
@ -95,7 +116,7 @@ export default class ChannelInfo extends PureComponent {
|
|||
};
|
||||
|
||||
actionsRows = (channelIsArchived) => {
|
||||
const {currentChannel, currentUserId, isDirectMessage, theme} = this.props;
|
||||
const {currentChannel, currentUserId, isDirectMessage, theme, isCallsEnabled, callsFeatureEnabled, isChannelAdmin} = this.props;
|
||||
|
||||
if (channelIsArchived) {
|
||||
return (
|
||||
|
|
@ -156,6 +177,24 @@ export default class ChannelInfo extends PureComponent {
|
|||
testID='channel_info.edit_channel.action'
|
||||
theme={theme}
|
||||
/>
|
||||
{callsFeatureEnabled &&
|
||||
<>
|
||||
<StartCall
|
||||
testID='channel_info.start_call.action'
|
||||
theme={theme}
|
||||
currentChannelId={currentChannel.id}
|
||||
currentChannelName={currentChannel.display_name}
|
||||
joinCall={this.startCallHandler}
|
||||
canStartCall={isCallsEnabled}
|
||||
/>
|
||||
<EnableDisableCalls
|
||||
testID='channel_info.start_call.action'
|
||||
theme={theme}
|
||||
onPress={this.toggleCalls}
|
||||
canEnableDisableCalls={isChannelAdmin}
|
||||
enabled={isCallsEnabled}
|
||||
/>
|
||||
</>}
|
||||
<Bindings
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,13 @@ describe('channelInfo', () => {
|
|||
getCustomEmojisInText: jest.fn(),
|
||||
showPermalink: jest.fn(),
|
||||
setChannelDisplayName: jest.fn(),
|
||||
joinCall: jest.fn(),
|
||||
enableChannelCalls: jest.fn(),
|
||||
disableChannelCalls: jest.fn(),
|
||||
},
|
||||
isCallsEnabled: false,
|
||||
callsFeatureEnabled: false,
|
||||
isChannelAdmin: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
|
|
@ -70,6 +76,39 @@ describe('channelInfo', () => {
|
|||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on calls features enabled, user is not admin and calls disabled for channel', async () => {
|
||||
const props = {...baseProps, isCallsEnabled: false, callsFeatureEnabled: true, isChannelAdmin: false};
|
||||
const wrapper = shallow(
|
||||
<ChannelInfo
|
||||
{...props}
|
||||
/>,
|
||||
{context: {intl: intlMock}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on calls features enabled and calls disabled for channel', async () => {
|
||||
const props = {...baseProps, isCallsEnabled: false, callsFeatureEnabled: true, isChannelAdmin: true};
|
||||
const wrapper = shallow(
|
||||
<ChannelInfo
|
||||
{...props}
|
||||
/>,
|
||||
{context: {intl: intlMock}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on calls features enabled and calls enabled for channel', async () => {
|
||||
const props = {...baseProps, isCallsEnabled: true, callsFeatureEnabled: true, isChannelAdmin: true};
|
||||
const wrapper = shallow(
|
||||
<ChannelInfo
|
||||
{...props}
|
||||
/>,
|
||||
{context: {intl: intlMock}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should dismiss modal on close', () => {
|
||||
const dismissModal = jest.spyOn(NavigationActions, 'dismissModal');
|
||||
const wrapper = shallow(
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import {getChannelStats} from '@mm-redux/actions/channels';
|
|||
import {getCustomEmojisInText} from '@mm-redux/actions/emojis';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getCurrentUserRoles, getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin, displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {joinCall, enableChannelCalls, disableChannelCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {isCallsEnabled} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired, isCustomStatusExpirySupported} from '@selectors/custom_status';
|
||||
import {isGuest} from '@utils/users';
|
||||
|
||||
|
|
@ -36,6 +39,8 @@ function makeMapStateToProps() {
|
|||
let customStatus;
|
||||
let customStatusExpired = true;
|
||||
let customStatusExpirySupported = false;
|
||||
const roles = getCurrentUserRoles(state) || '';
|
||||
const isChannelAdmin = checkIsAdmin(roles) || checkIsChannelAdmin(roles);
|
||||
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
|
||||
|
||||
if (isDirectMessage) {
|
||||
|
|
@ -55,6 +60,8 @@ function makeMapStateToProps() {
|
|||
currentChannelMemberCount = currentChannel.display_name.split(',').length;
|
||||
}
|
||||
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
|
||||
return {
|
||||
currentChannel,
|
||||
currentChannelCreatorName,
|
||||
|
|
@ -69,6 +76,9 @@ function makeMapStateToProps() {
|
|||
isCustomStatusEnabled: customStatusEnabled,
|
||||
isCustomStatusExpired: customStatusExpired,
|
||||
isCustomStatusExpirySupported: customStatusExpirySupported,
|
||||
isCallsEnabled: isCallsEnabled(state),
|
||||
callsFeatureEnabled,
|
||||
isChannelAdmin,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -79,6 +89,9 @@ function mapDispatchToProps(dispatch) {
|
|||
getChannelStats,
|
||||
getCustomEmojisInText,
|
||||
setChannelDisplayName,
|
||||
joinCall,
|
||||
enableChannelCalls,
|
||||
disableChannelCalls,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case 'MainSidebar':
|
||||
screen = require('app/components/sidebars/main').default;
|
||||
break;
|
||||
case 'Call':
|
||||
screen = require('@mmproducts/calls/screens/call').default;
|
||||
break;
|
||||
case 'MFA':
|
||||
screen = require('@screens/mfa').default;
|
||||
break;
|
||||
|
|
@ -169,6 +172,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case 'PostOptions':
|
||||
screen = require('@screens/post_options').default;
|
||||
break;
|
||||
case 'CallOtherActions':
|
||||
screen = require('@mmproducts/calls/screens/call_other_actions').default;
|
||||
break;
|
||||
case 'ReactionList':
|
||||
screen = require('@screens/reaction_list').default;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -18,16 +18,24 @@ type Props = {
|
|||
currentUserId: $ID<UserProfile>;
|
||||
userProfiles: UserProfile[];
|
||||
teammateNameDisplay: string;
|
||||
listTitle?: JSX.Element;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const ParticipantsList = ({currentUserId, teammateNameDisplay, theme, userProfiles}: Props) => {
|
||||
const ParticipantsList = ({currentUserId, teammateNameDisplay, theme, userProfiles, listTitle}: Props) => {
|
||||
const close = () => {
|
||||
dismissModal();
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
const style = getStyleSheet(theme);
|
||||
if (listTitle) {
|
||||
return (
|
||||
<View style={style.header}>
|
||||
{listTitle}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={style.header}>
|
||||
<FormattedText
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ const viewsBlackListFilter = createBlacklistFilter(
|
|||
['extension', 'root'],
|
||||
);
|
||||
|
||||
const typingBlackListFilter = createBlacklistFilter(
|
||||
const entitiesBlackListFilter = createBlacklistFilter(
|
||||
'entities',
|
||||
['typing'],
|
||||
['typing', 'calls'],
|
||||
);
|
||||
|
||||
const channelViewBlackList: any = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
|
||||
|
|
@ -174,7 +174,7 @@ export default function configureStore(storage: any, preloadedState: any = {}, o
|
|||
transforms: [
|
||||
setTransformer,
|
||||
viewsBlackListFilter,
|
||||
typingBlackListFilter,
|
||||
entitiesBlackListFilter,
|
||||
channelViewBlackListFilter,
|
||||
emojiBlackListFilter,
|
||||
],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -56,6 +56,27 @@
|
|||
"apps.suggestion.no_static": "No matching options.",
|
||||
"apps.suggestion.no_suggestion": "No matching suggestions.",
|
||||
"archivedChannelMessage": "You are viewing an **archived channel**. New messages cannot be posted.",
|
||||
"call_message.call_ended": "Call ended",
|
||||
"call_message.call_ended_at": "Ended at {time}",
|
||||
"call_message.call_lasted": "Lasted {duration}",
|
||||
"call_message.current_call": "Current Call",
|
||||
"call_message.join_call": "Join Call",
|
||||
"call_message.started_a_call": "{user} started a call",
|
||||
"call.add_participants": "Add participants",
|
||||
"call.chat_thread": "Chat thread",
|
||||
"call.copy_call_link": "Copy call link",
|
||||
"call.give_feedback": "Give Feedback",
|
||||
"call.leave": "Leave",
|
||||
"call.more": "More",
|
||||
"call.mute": "Mute",
|
||||
"call.screen_share_user": "You are seing {userDisplayName} screen",
|
||||
"call.settings": "Settings",
|
||||
"call.unmute": "Unmute",
|
||||
"calls.confirm-to-join-cancel": "Cancel",
|
||||
"calls.confirm-to-join-description": "You are already on a channel call in ~{callChannelName}. Do you want to leave your current call and join the call in ~{currentChannelName}?",
|
||||
"calls.confirm-to-join-leave-and-join": "Leave & Join",
|
||||
"calls.confirm-to-join-title": "Are you sure you want to switch to a different call?",
|
||||
"calls.join_call.participants_list_header": "CALL PARTICIPANTS",
|
||||
"camera_type.photo.option": "Capture Photo",
|
||||
"camera_type.title": "Choose an action",
|
||||
"camera_type.video.option": "Record Video",
|
||||
|
|
@ -134,6 +155,8 @@
|
|||
"create_comment.addComment": "Add a comment...",
|
||||
"create_post.deactivated": "You are viewing an archived channel with a deactivated user.",
|
||||
"create_post.write": "Write to {channelDisplayName}",
|
||||
"current_call.channel-name": "~{channelName}",
|
||||
"current_call.user-is-speaking": "{username} is speaking",
|
||||
"custom_status.expiry_dropdown.custom": "Custom",
|
||||
"custom_status.expiry_dropdown.date_and_time": "Date and Time",
|
||||
"custom_status.expiry_dropdown.dont_clear": "Don't clear",
|
||||
|
|
@ -294,9 +317,13 @@
|
|||
"mobile.channel_info.copy_header": "Copy Header",
|
||||
"mobile.channel_info.copy_purpose": "Copy Purpose",
|
||||
"mobile.channel_info.delete_failed": "We couldn't archive the channel {displayName}. Please check your connection and try again.",
|
||||
"mobile.channel_info.disable_calls": "Disable Calls",
|
||||
"mobile.channel_info.edit": "Edit Channel",
|
||||
"mobile.channel_info.enable_calls": "Enable Calls",
|
||||
"mobile.channel_info.join_ongoing_call": "Join Ongoing Call",
|
||||
"mobile.channel_info.privateChannel": "Private Channel",
|
||||
"mobile.channel_info.publicChannel": "Public Channel",
|
||||
"mobile.channel_info.start_call": "Start Call",
|
||||
"mobile.channel_info.unarchive_failed": "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
|
||||
"mobile.channel_list.alertNo": "No",
|
||||
"mobile.channel_list.alertYes": "Yes",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -22,6 +22,7 @@ module.exports = {
|
|||
'@init': './app/init',
|
||||
'@mattermost-managed': './app/mattermost_managed/index',
|
||||
'@mm-redux': './app/mm-redux',
|
||||
'@mmproducts': './app/products',
|
||||
'@share': './share_extension',
|
||||
'@screens': './app/screens',
|
||||
'@selectors': './app/selectors',
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@
|
|||
AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */,
|
||||
7FFE32A91FD9CB650038C7A0 /* Embed App Extensions */,
|
||||
ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */,
|
||||
E23E84F30F683B193780BB30 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -743,6 +744,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "./uploadDebugSymbols.sh\n";
|
||||
};
|
||||
E23E84F30F683B193780BB30 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Mattermost/Pods-Mattermost-frameworks.sh",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-webrtc2/ios/WebRTC.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mattermost/Pods-Mattermost-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
ED4C644925C525E30315E09E /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -1170,7 +1189,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 ";
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
|
|
@ -1226,7 +1245,7 @@
|
|||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 ";
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
|
|
|||
|
|
@ -276,6 +276,8 @@ PODS:
|
|||
- react-native-video/Video (= 5.2.0)
|
||||
- react-native-video/Video (5.2.0):
|
||||
- React-Core
|
||||
- react-native-webrtc2 (1.84.5):
|
||||
- React
|
||||
- react-native-webview (11.14.2):
|
||||
- React-Core
|
||||
- React-perflogger (0.66.1)
|
||||
|
|
@ -345,6 +347,8 @@ PODS:
|
|||
- React-perflogger (= 0.66.1)
|
||||
- ReactNativeExceptionHandler (2.10.10):
|
||||
- React-Core
|
||||
- ReactNativeIncallManager (3.3.0):
|
||||
- React
|
||||
- ReactNativeKeyboardTrackingView (5.7.0):
|
||||
- React
|
||||
- ReactNativeNavigation (7.23.1):
|
||||
|
|
@ -493,6 +497,7 @@ DEPENDENCIES:
|
|||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- react-native-startup-time (from `../node_modules/react-native-startup-time`)
|
||||
- react-native-video (from `../node_modules/react-native-video`)
|
||||
- react-native-webrtc2 (from `../node_modules/react-native-webrtc2`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||
|
|
@ -507,6 +512,7 @@ DEPENDENCIES:
|
|||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- ReactNativeExceptionHandler (from `../node_modules/react-native-exception-handler`)
|
||||
- ReactNativeIncallManager (from `../node_modules/react-native-incall-manager`)
|
||||
- ReactNativeKeyboardTrackingView (from `../node_modules/react-native-keyboard-tracking-view`)
|
||||
- ReactNativeNavigation (from `../node_modules/react-native-navigation`)
|
||||
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
||||
|
|
@ -631,6 +637,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-startup-time"
|
||||
react-native-video:
|
||||
:path: "../node_modules/react-native-video"
|
||||
react-native-webrtc2:
|
||||
:path: "../node_modules/react-native-webrtc2"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
React-perflogger:
|
||||
|
|
@ -659,6 +667,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native/ReactCommon"
|
||||
ReactNativeExceptionHandler:
|
||||
:path: "../node_modules/react-native-exception-handler"
|
||||
ReactNativeIncallManager:
|
||||
:path: "../node_modules/react-native-incall-manager"
|
||||
ReactNativeKeyboardTrackingView:
|
||||
:path: "../node_modules/react-native-keyboard-tracking-view"
|
||||
ReactNativeNavigation:
|
||||
|
|
@ -764,6 +774,7 @@ SPEC CHECKSUMS:
|
|||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||
react-native-startup-time: 1a068b744ce5097a85ebe0fbff691b05961c324d
|
||||
react-native-video: a4c2635d0802f983594b7057e1bce8f442f0ad28
|
||||
react-native-webrtc2: b3f1d5504c8bbd3aa4ce9b6c3ce0fbc47c7e146a
|
||||
react-native-webview: cc4ac8f021a832dce2979e1f48c5595341dd2fb5
|
||||
React-perflogger: fcac6090a80e3d967791b4c7f1b1a017f9d4a398
|
||||
React-RCTActionSheet: caf5913d9f9e605f5467206cf9d1caa6d47d7ad6
|
||||
|
|
@ -778,6 +789,7 @@ SPEC CHECKSUMS:
|
|||
React-runtimeexecutor: 33a949a51bec5f8a3c9e8d8092deb259600d761e
|
||||
ReactCommon: 620442811dc6f707b4bf5e3b27d4f19c12d5a821
|
||||
ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60
|
||||
ReactNativeIncallManager: a840ef75640518065742a893e6c6ff98ee820039
|
||||
ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306
|
||||
ReactNativeNavigation: 3ab7ff832654964985de47c9ee67999e34cd63d8
|
||||
rn-fetch-blob: 17961aec08caae68bb8fc0e5b40f93b3acfa6932
|
||||
|
|
|
|||
892
package-lock.json
generated
892
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue