Compare commits

...

4 Commits

Author SHA1 Message Date
Elias Nahum
21ac2b3168 Bump app build number to 397 (#6235) 2022-05-05 14:22:29 -04:00
Mattermost Build
491209fdae MM-43904 - Fix: Calls batch actions (#6229) (#6233)
* fix batch actions

* tests

(cherry picked from commit 67c65156a7)

Co-authored-by: Christopher Poile <cpoile@gmail.com>
2022-05-05 14:13:30 -04:00
Elias Nahum
696f8d69a3 Dot release 1.51.2 (#6217)
* Bump app version number to  1.51.2

* Bump app build number to  395
2022-05-03 18:10:42 -04:00
Mattermost Build
0e06eb60fb MM-43904 - Fix: Calls: "Access to route for non-existent plugin" error log (#6210) (#6213)
* add isCallsPluginEnabled; refactor Calls.PluginId

* revert Podfile.lock changes

(cherry picked from commit 912287fbe0)

Co-authored-by: Christopher Poile <cpoile@gmail.com>
2022-05-03 15:10:40 -04:00
24 changed files with 166 additions and 56 deletions

View File

@@ -131,8 +131,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 392
versionName "1.51.1"
versionCode 397
versionName "1.51.2"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -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

View File

@@ -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();

View File

@@ -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<ClientPluginManifest[]>;
}
const ClientPlugins = (superclass: any) => class extends superclass {
getPluginsManifests = async () => {
return this.doFetch(
`${this.getPluginsRoute()}/webapp`,
{method: 'get'},
);
};
};
export default ClientPlugins;

View File

@@ -10,4 +10,6 @@ const RequiredServer = {
PATCH_VERSION: 0,
};
export default {RequiredServer, RefreshConfigMillis};
const PluginId = 'com.mattermost.calls';
export default {RequiredServer, RefreshConfigMillis, PluginId};

View File

@@ -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;

View File

@@ -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;

View File

@@ -24,4 +24,5 @@ export default keyMirror({
SET_SCREENSHARE_URL: null,
SET_SPEAKERPHONE: null,
RECEIVED_CONFIG: null,
RECEIVED_PLUGIN_ENABLED: null,
});

View File

@@ -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),
},
@@ -135,14 +141,14 @@ describe('Actions.Calls', () => {
});
it('loadCalls', async () => {
await store.dispatch(await store.dispatch(CallsActions.loadCalls()));
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('loadConfig', async () => {
await store.dispatch(await store.dispatch(CallsActions.loadConfig()));
await store.dispatch(CallsActions.loadConfig());
expect(Client4.getCallsConfig).toBeCalledWith();
assert.equal(store.getState().entities.calls.config.DefaultEnabled, true);
assert.equal(store.getState().entities.calls.config.AllowEnableCalls, true);
@@ -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();

View File

@@ -3,12 +3,19 @@
import {intlShape} from 'react-intl';
import InCallManager from 'react-native-incall-manager';
import {batch} from 'react-redux';
import {Client4} from '@client/rest';
import Calls from '@constants/calls';
import {logError} from '@mm-redux/actions/errors';
import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers';
import {GenericAction, ActionFunc, DispatchFunc, GetStateFunc, batchActions} from '@mm-redux/types/actions';
import {
GenericAction,
ActionFunc,
DispatchFunc,
GetStateFunc,
ActionResult,
} 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';
@@ -19,7 +26,7 @@ import {hasMicrophonePermission} from '@utils/permission';
export let ws: any = null;
export function loadConfig(force = false): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<GenericAction> => {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
if (!force) {
if ((Date.now() - getConfig(getState()).last_retrieved_at) < Calls.RefreshConfigMillis) {
return {} as GenericAction;
@@ -34,28 +41,27 @@ export function loadConfig(force = false): ActionFunc {
dispatch(logError(error));
// Reset the config to the default (off) since it looks like Calls is not enabled.
return {
dispatch({
type: CallsTypes.RECEIVED_CONFIG,
data: {...DefaultServerConfig, last_retrieved_at: Date.now()},
};
});
}
return {
type: CallsTypes.RECEIVED_CONFIG,
data: {...data, last_retrieved_at: Date.now()},
};
data = {...data, last_retrieved_at: Date.now()};
dispatch({type: CallsTypes.RECEIVED_CONFIG, data});
return {data};
};
}
export function loadCalls(): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<GenericAction> => {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
let resp = [];
try {
resp = await Client4.getCalls();
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {} as GenericAction;
return {};
}
const callsResults: Dictionary<Call> = {};
@@ -86,21 +92,46 @@ export function loadCalls(): ActionFunc {
enabled: enabledChannels,
};
return {type: CallsTypes.RECEIVED_CALLS, data};
dispatch({type: CallsTypes.RECEIVED_CALLS, data});
return {data};
};
}
export function batchLoadCalls(forceConfig = false): ActionFunc {
return async (dispatch: DispatchFunc) => {
const promises = [dispatch(loadConfig(forceConfig)), dispatch(loadCalls())];
Promise.all(promises).then((actions: Array<Awaited<GenericAction>>) => {
dispatch(batchActions(actions, 'BATCH_LOAD_CALLS'));
const res = await dispatch(checkIsCallsPluginEnabled());
if (!res.data) {
// Calls is not enabled.
return {};
}
batch(() => {
dispatch(loadConfig(forceConfig));
dispatch(loadCalls());
});
return {};
};
}
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 +166,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'};

View File

@@ -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,
});

View File

@@ -61,3 +61,7 @@ export function isSupportedServer(state: GlobalState) {
return false;
}
export function isCallsPluginEnabled(state: GlobalState) {
return state.entities.calls.pluginEnabled;
}

View File

@@ -11,6 +11,7 @@ export type CallsState = {
screenShareURL: string;
speakerphoneOn: boolean;
config: ServerConfig;
pluginEnabled: boolean;
}
export type Call = {

View File

@@ -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();

View File

@@ -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}
<NetworkIndicator/>
<AnnouncementBanner/>
{isSupportedServerCalls &&
{isCallsEnabled &&
<FloatingCallContainer>
<JoinCall/>
<CurrentCall/>
</FloatingCallContainer>}
</FloatingCallContainer>
}
</>
);

View File

@@ -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 {
<>
<AnnouncementBanner/>
<NetworkIndicator/>
{isSupportedServerCalls &&
{isCallsEnabled &&
<FloatingCallContainer>
<JoinCall/>
<CurrentCall/>
</FloatingCallContainer>}
</FloatingCallContainer>
}
</>
);
const header = (

View File

@@ -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,

View File

@@ -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,
};
}

View File

@@ -909,7 +909,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 392;
CURRENT_PROJECT_VERSION = 397;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@@ -951,7 +951,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 392;
CURRENT_PROJECT_VERSION = 397;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.51.1</string>
<string>1.51.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>392</string>
<string>397</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.51.1</string>
<string>1.51.2</string>
<key>CFBundleVersion</key>
<string>392</string>
<string>397</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.51.1</string>
<string>1.51.2</string>
<key>CFBundleVersion</key>
<string>392</string>
<string>397</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.51.1",
"version": "1.51.2",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.51.1",
"version": "1.51.2",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",