diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 9dbb1f31e..4a515dcf4 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -7,6 +7,7 @@ import {RNFetchBlobFetchRepsonse} from 'rn-fetch-blob'; import urlParse from 'url-parse'; +import Calls from '@constants/calls'; import {Options} from '@mm-redux/types/client4'; import * as ClientConstants from './constants'; @@ -286,12 +287,16 @@ export default class ClientBase { return `${this.getUserThreadsRoute(userId, teamId)}/${threadId}`; } + getPluginsRoute() { + return `${this.getBaseRoute()}/plugins`; + } + getAppsProxyRoute() { return `${this.url}/plugins/com.mattermost.apps`; } getCallsRoute() { - return `${this.url}/plugins/com.mattermost.calls`; + return `${this.url}/plugins/${Calls.PluginId}`; } // Client Helpers diff --git a/app/client/rest/index.ts b/app/client/rest/index.ts index 2b125c9ba..8198829d6 100644 --- a/app/client/rest/index.ts +++ b/app/client/rest/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import ClientPlugins, {ClientPluginsMix} from '@client/rest/plugins'; import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest'; import mix from '@utils/mix'; @@ -36,7 +37,8 @@ interface Client extends ClientBase, ClientTeamsMix, ClientTosMix, ClientUsersMix, - ClientCallsMix + ClientCallsMix, + ClientPluginsMix {} class Client extends mix(ClientBase).with( @@ -55,6 +57,7 @@ class Client extends mix(ClientBase).with( ClientTos, ClientUsers, ClientCalls, + ClientPlugins, ) {} const Client4 = new Client(); diff --git a/app/client/rest/plugins.ts b/app/client/rest/plugins.ts new file mode 100644 index 000000000..c81171a44 --- /dev/null +++ b/app/client/rest/plugins.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ClientPluginManifest} from '@mm-redux/types/plugins'; + +export interface ClientPluginsMix { + getPluginsManifests: () => Promise; +} + +const ClientPlugins = (superclass: any) => class extends superclass { + getPluginsManifests = async () => { + return this.doFetch( + `${this.getPluginsRoute()}/webapp`, + {method: 'get'}, + ); + }; +}; + +export default ClientPlugins; diff --git a/app/constants/calls.ts b/app/constants/calls.ts index 197f95029..ec17fde4d 100644 --- a/app/constants/calls.ts +++ b/app/constants/calls.ts @@ -10,4 +10,6 @@ const RequiredServer = { PATCH_VERSION: 0, }; -export default {RequiredServer, RefreshConfigMillis}; +const PluginId = 'com.mattermost.calls'; + +export default {RequiredServer, RefreshConfigMillis, PluginId}; diff --git a/app/constants/websocket.ts b/app/constants/websocket.ts index 9be370735..11004aca6 100644 --- a/app/constants/websocket.ts +++ b/app/constants/websocket.ts @@ -1,5 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. + +import Calls from '@constants/calls'; + const WebsocketEvents = { POSTED: 'posted', POST_EDITED: 'post_edited', @@ -53,18 +56,18 @@ 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', - CALLS_USER_RAISE_HAND: 'custom_com.mattermost.calls_user_raise_hand', - CALLS_USER_UNRAISE_HAND: 'custom_com.mattermost.calls_user_unraise_hand', + CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`, + CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`, + CALLS_USER_CONNECTED: `custom_${Calls.PluginId}_user_connected`, + CALLS_USER_DISCONNECTED: `custom_${Calls.PluginId}_user_disconnected`, + CALLS_USER_MUTED: `custom_${Calls.PluginId}_user_muted`, + CALLS_USER_UNMUTED: `custom_${Calls.PluginId}_user_unmuted`, + CALLS_USER_VOICE_ON: `custom_${Calls.PluginId}_user_voice_on`, + CALLS_USER_VOICE_OFF: `custom_${Calls.PluginId}_user_voice_off`, + CALLS_CALL_START: `custom_${Calls.PluginId}_call_start`, + CALLS_SCREEN_ON: `custom_${Calls.PluginId}_user_screen_on`, + CALLS_SCREEN_OFF: `custom_${Calls.PluginId}_user_screen_off`, + CALLS_USER_RAISE_HAND: `custom_${Calls.PluginId}_user_raise_hand`, + CALLS_USER_UNRAISE_HAND: `custom_${Calls.PluginId}_user_unraise_hand`, }; export default WebsocketEvents; diff --git a/app/products/calls/hooks.ts b/app/products/calls/hooks.ts index a1a70e04c..d6911a7e1 100644 --- a/app/products/calls/hooks.ts +++ b/app/products/calls/hooks.ts @@ -11,7 +11,12 @@ import {getCurrentChannel} from '@mm-redux/selectors/entities/channels'; import {getCurrentUserRoles} from '@mm-redux/selectors/entities/users'; import {isAdmin as checkIsAdmin, isChannelAdmin as checkIsChannelAdmin} from '@mm-redux/utils/user_utils'; import {loadConfig} from '@mmproducts/calls/store/actions/calls'; -import {getConfig, isCallsExplicitlyDisabled, isCallsExplicitlyEnabled} from '@mmproducts/calls/store/selectors/calls'; +import { + getConfig, + isCallsExplicitlyDisabled, + isCallsExplicitlyEnabled, + isCallsPluginEnabled, +} from '@mmproducts/calls/store/selectors/calls'; // Check if calls is enabled. If it is, then run fn; if it isn't, show an alert and set // msgPostfix to ' (Not Available)'. @@ -45,12 +50,15 @@ export const useCallsChannelSettings = () => { const dispatch = useDispatch(); const config = useSelector(getConfig); const currentChannel = useSelector(getCurrentChannel); + const pluginEnabled = useSelector(isCallsPluginEnabled); const explicitlyDisabled = useSelector(isCallsExplicitlyDisabled); const explicitlyEnabled = useSelector(isCallsExplicitlyEnabled); const roles = useSelector(getCurrentUserRoles); useEffect(() => { - dispatch(loadConfig()); + if (pluginEnabled) { + dispatch(loadConfig()); + } }, []); const isDirectMessage = currentChannel.type === General.DM_CHANNEL; @@ -58,9 +66,11 @@ export const useCallsChannelSettings = () => { const isAdmin = checkIsAdmin(roles); const isChannelAdmin = isAdmin || checkIsChannelAdmin(roles); - const enabled = (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled)); + const enabled = pluginEnabled && (explicitlyEnabled || (!explicitlyDisabled && config.DefaultEnabled)); let canEnableDisable; - if (config.AllowEnableCalls) { + if (!pluginEnabled) { + canEnableDisable = false; + } else if (config.AllowEnableCalls) { canEnableDisable = isDirectMessage || isGroupMessage || isChannelAdmin; } else { canEnableDisable = isAdmin; diff --git a/app/products/calls/store/action_types/calls.ts b/app/products/calls/store/action_types/calls.ts index b438fdde8..753880754 100644 --- a/app/products/calls/store/action_types/calls.ts +++ b/app/products/calls/store/action_types/calls.ts @@ -24,4 +24,5 @@ export default keyMirror({ SET_SCREENSHARE_URL: null, SET_SPEAKERPHONE: null, RECEIVED_CONFIG: null, + RECEIVED_PLUGIN_ENABLED: null, }); diff --git a/app/products/calls/store/actions/calls.test.js b/app/products/calls/store/actions/calls.test.js index afb307e1b..741aaba46 100644 --- a/app/products/calls/store/actions/calls.test.js +++ b/app/products/calls/store/actions/calls.test.js @@ -39,6 +39,12 @@ jest.mock('@client/rest', () => ({ DefaultEnabled: true, last_retrieved_at: 1234, })), + getPluginsManifests: jest.fn(() => ( + [ + {id: 'playbooks'}, + {id: 'com.mattermost.calls'}, + ] + )), enableChannelCalls: jest.fn(() => null), disableChannelCalls: jest.fn(() => null), }, @@ -150,6 +156,7 @@ describe('Actions.Calls', () => { it('batchLoadConfig', async () => { await store.dispatch(CallsActions.batchLoadCalls()); + expect(Client4.getPluginsManifests).toBeCalledWith(); expect(Client4.getCallsConfig).toBeCalledWith(); expect(Client4.getCalls).toBeCalledWith(); diff --git a/app/products/calls/store/actions/calls.ts b/app/products/calls/store/actions/calls.ts index abfe2e610..8b01cf86a 100644 --- a/app/products/calls/store/actions/calls.ts +++ b/app/products/calls/store/actions/calls.ts @@ -92,6 +92,12 @@ export function loadCalls(): ActionFunc { export function batchLoadCalls(forceConfig = false): ActionFunc { return async (dispatch: DispatchFunc) => { + const res = await dispatch(checkIsCallsPluginEnabled()); + if (!res.data) { + // Calls is not enabled. + return {}; + } + const promises = [dispatch(loadConfig(forceConfig)), dispatch(loadCalls())]; Promise.all(promises).then((actions: Array>) => { dispatch(batchActions(actions, 'BATCH_LOAD_CALLS')); @@ -101,6 +107,24 @@ export function batchLoadCalls(forceConfig = false): ActionFunc { }; } +export function checkIsCallsPluginEnabled(): ActionFunc { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + let data; + try { + data = await Client4.getPluginsManifests(); + } catch (error) { + forceLogoutIfNecessary(error, dispatch, getState); + dispatch(logError(error)); + return {} as GenericAction; + } + + const enabled = data.findIndex((m) => m.id === Calls.PluginId) !== -1; + dispatch({type: CallsTypes.RECEIVED_PLUGIN_ENABLED, data: enabled}); + + return {data: enabled}; + }; +} + export function enableChannelCalls(channelId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { try { @@ -135,6 +159,10 @@ export function disableChannelCalls(channelId: string): ActionFunc { export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + // Edge case: calls was disabled when app loaded, and then enabled, but app hasn't + // reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet) + dispatch(checkIsCallsPluginEnabled()); + const hasPermission = await hasMicrophonePermission(intl); if (!hasPermission) { return {error: 'no permissions to microphone, unable to start call'}; diff --git a/app/products/calls/store/reducers/calls.ts b/app/products/calls/store/reducers/calls.ts index 0ead8e7ea..b2b61775a 100644 --- a/app/products/calls/store/reducers/calls.ts +++ b/app/products/calls/store/reducers/calls.ts @@ -216,6 +216,16 @@ function speakerphoneOn(state = false, action: GenericAction) { } } +function pluginEnabled(state = false, action: GenericAction) { + switch (action.type) { + case CallsTypes.RECEIVED_PLUGIN_ENABLED: { + return action.data; + } + default: + return state; + } +} + export default combineReducers({ calls, enabled, @@ -223,4 +233,5 @@ export default combineReducers({ screenShareURL, speakerphoneOn, config, + pluginEnabled, }); diff --git a/app/products/calls/store/selectors/calls.ts b/app/products/calls/store/selectors/calls.ts index 92a975235..88de09335 100644 --- a/app/products/calls/store/selectors/calls.ts +++ b/app/products/calls/store/selectors/calls.ts @@ -61,3 +61,7 @@ export function isSupportedServer(state: GlobalState) { return false; } + +export function isCallsPluginEnabled(state: GlobalState) { + return state.entities.calls.pluginEnabled; +} diff --git a/app/products/calls/store/types/calls.ts b/app/products/calls/store/types/calls.ts index 9390de8f6..1f0a0591e 100644 --- a/app/products/calls/store/types/calls.ts +++ b/app/products/calls/store/types/calls.ts @@ -11,6 +11,7 @@ export type CallsState = { screenShareURL: string; speakerphoneOn: boolean; config: ServerConfig; + pluginEnabled: boolean; } export type Call = { diff --git a/app/products/calls/websocket.ts b/app/products/calls/websocket.ts index 69536a5c4..286a99957 100644 --- a/app/products/calls/websocket.ts +++ b/app/products/calls/websocket.ts @@ -2,13 +2,14 @@ // See LICENSE.txt for license information. import {EventEmitter} from 'events'; +import Calls from '@constants/calls'; import {encode} from '@msgpack/msgpack/dist'; export default class WebSocketClient extends EventEmitter { private ws: WebSocket | null; private seqNo = 0; private connID = ''; - private eventPrefix = 'custom_com.mattermost.calls'; + private eventPrefix = `custom_${Calls.PluginId}`; constructor(connURL: string) { super(); diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js index 716f33c19..2cceef403 100644 --- a/app/screens/channel/channel.android.js +++ b/app/screens/channel/channel.android.js @@ -64,7 +64,7 @@ export default class ChannelAndroid extends ChannelBase { } render() { - const {theme, viewingGlobalThreads, isSupportedServerCalls} = this.props; + const {theme, viewingGlobalThreads, isCallsEnabled} = this.props; let component; if (viewingGlobalThreads) { @@ -106,11 +106,12 @@ export default class ChannelAndroid extends ChannelBase { {component} - {isSupportedServerCalls && + {isCallsEnabled && - } + + } ); diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js index fe98abf08..40ce5f36e 100644 --- a/app/screens/channel/channel.ios.js +++ b/app/screens/channel/channel.ios.js @@ -57,7 +57,7 @@ export default class ChannelIOS extends ChannelBase { }; render() { - const {currentChannelId, theme, viewingGlobalThreads, isSupportedServerCalls} = this.props; + const {currentChannelId, theme, viewingGlobalThreads, isCallsEnabled} = this.props; let component; let renderDraftArea = false; @@ -85,11 +85,12 @@ export default class ChannelIOS extends ChannelBase { <> - {isSupportedServerCalls && + {isCallsEnabled && - } + + } ); const header = ( diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index 68b3c0bf1..2d0e08b41 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -40,6 +40,7 @@ export default class ChannelBase extends PureComponent { viewingGlobalThreads: PropTypes.bool, collapsedThreadsEnabled: PropTypes.bool.isRequired, isSupportedServerCalls: PropTypes.bool.isRequired, + isCallsEnabled: PropTypes.bool.isRequired, selectedPost: PropTypes.shape({ id: PropTypes.string.isRequired, channel_id: PropTypes.string.isRequired, diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 3ad55fd2a..85940886c 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -18,7 +18,10 @@ import {getCurrentUserId, getCurrentUserRoles, shouldShowTermsOfService} from '@ import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {isSystemAdmin as checkIsSystemAdmin} from '@mm-redux/utils/user_utils'; import {batchLoadCalls} from '@mmproducts/calls/store/actions/calls'; -import {isSupportedServer as isSupportedServerForCalls} from '@mmproducts/calls/store/selectors/calls'; +import { + isCallsPluginEnabled, + isSupportedServer as isSupportedServerForCalls, +} from '@mmproducts/calls/store/selectors/calls'; import {getViewingGlobalThreads} from '@selectors/threads'; import Channel from './channel'; @@ -44,6 +47,7 @@ function mapStateToProps(state) { const currentChannelId = currentTeam?.delete_at === 0 ? getCurrentChannelId(state) : ''; const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state); const isSupportedServerCalls = isSupportedServerForCalls(state); + const isCallsEnabled = isCallsPluginEnabled(state); return { currentChannelId, @@ -58,6 +62,7 @@ function mapStateToProps(state) { theme: getTheme(state), viewingGlobalThreads: collapsedThreadsEnabled && getViewingGlobalThreads(state), isSupportedServerCalls, + isCallsEnabled, }; }