diff --git a/components/call_button/call_button.jsx b/components/call_button/call_button.jsx
new file mode 100644
index 000000000000..da0c95cc19df
--- /dev/null
+++ b/components/call_button/call_button.jsx
@@ -0,0 +1,111 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {injectIntl} from 'react-intl';
+
+import {intlShape} from 'utils/react_intl';
+
+import MenuWrapper from 'components/widgets/menu/menu_wrapper';
+import Menu from 'components/widgets/menu/menu';
+import CameraIcon from 'components/widgets/icons/camera_icon';
+
+const customStyles = {
+ left: 'inherit',
+ right: 0,
+ bottom: '100%',
+ top: 'auto',
+};
+
+function CallButton(props) {
+ const {formatMessage} = props.intl;
+
+ let bodyAction;
+
+ if (props.pluginCallMethods.length === 0) {
+ bodyAction = null;
+ } else if (props.pluginCallMethods.length === 1) {
+ const item = props.pluginCallMethods[0];
+ bodyAction = (
+
+ );
+ } else {
+ const pluginCallMethods = props.pluginCallMethods.map((item) => {
+ return (
+
{
+ e.preventDefault();
+ if (item.action) {
+ item.action(props.currentChannel, props.channelMember);
+ }
+ }}
+ >
+
+
+ {item.icon}
+
+ {item.dropdownText}
+
+
+ );
+ });
+ bodyAction = (
+
+
+
+
+ );
+ }
+
+ return bodyAction;
+}
+
+CallButton.propTypes = {
+ currentChannel: PropTypes.object.isRequired,
+ channelMember: PropTypes.object,
+ intl: intlShape.isRequired,
+ locale: PropTypes.string.isRequired,
+ pluginCallMethods: PropTypes.arrayOf(PropTypes.object),
+};
+
+CallButton.defaultProps = {
+ pluginCallMethods: [],
+};
+
+const wrappedComponent = injectIntl(CallButton);
+wrappedComponent.displayName = 'injectIntl(CallButton)';
+export default wrappedComponent;
diff --git a/components/call_button/index.js b/components/call_button/index.js
new file mode 100644
index 000000000000..2b8203d49e15
--- /dev/null
+++ b/components/call_button/index.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
+
+import {getCurrentLocale} from 'selectors/i18n';
+
+import CallButton from './call_button.jsx';
+
+function mapStateToProps(state) {
+ return {
+ currentChannel: getCurrentChannel(state),
+ locale: getCurrentLocale(state),
+ pluginCallMethods: state.plugins.components.CallButton,
+ channelMember: getMyCurrentChannelMembership(state),
+ };
+}
+
+export default connect(mapStateToProps)(CallButton);
diff --git a/components/create_post/__snapshots__/create_post.test.jsx.snap b/components/create_post/__snapshots__/create_post.test.jsx.snap
index bbcec12bd0f8..e42f5764a295 100644
--- a/components/create_post/__snapshots__/create_post.test.jsx.snap
+++ b/components/create_post/__snapshots__/create_post.test.jsx.snap
@@ -46,6 +46,7 @@ exports[`components/create_post Show tutorial 1`] = `
+
+
+
+
+
+ );
+ }
+
let fileUpload;
if (!readOnlyChannel && !this.props.shouldShowPreview) {
fileUpload = (
@@ -1519,6 +1527,7 @@ class CreatePost extends React.PureComponent {
ref='createPostControls'
className='post-body__actions'
>
+ {callButton}
{fileUpload}
{emojiPicker}
+
+
+ );
+}
diff --git a/i18n/en.json b/i18n/en.json
index a6216162a3c8..1bf041143367 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -2106,6 +2106,7 @@
"bots.token.confirm": "Delete",
"bots.token.confirm_text": "Are you sure you want to delete the token?",
"bots.token.delete": "Delete Token",
+ "call_button.menuAriaLabel": "Call type selector",
"center_panel.archived.closeChannel": "Close Channel",
"center_panel.direct.closeDirectMessage": "Close Direct Message",
"center_panel.direct.closeGroupMessage": "Close Group Message",
@@ -2549,6 +2550,7 @@
"generic_icons.back": "Back Icon",
"generic_icons.bot": "Bot Icon",
"generic_icons.breadcrumb": "Breadcrumb Icon",
+ "generic_icons.camera": "Camera Icon",
"generic_icons.channel.arrow-right": "Arrow right",
"generic_icons.channel.draft": "Channel Draft Icon",
"generic_icons.channel.private": "Private Channel Icon",
diff --git a/plugins/registry.js b/plugins/registry.js
index ec58a43309e9..597e583e5f2a 100644
--- a/plugins/registry.js
+++ b/plugins/registry.js
@@ -132,6 +132,42 @@ export default class PluginRegistry {
return id;
}
+ // Add a "call button"" next to the attach file button. If there are more than one button registered by any
+ // plugin, a dropdown menu is created to contain all the call plugin buttons.
+ // Accepts the following:
+ // - icon - React element to use as the button's icon
+ // - action - a function called when the button is clicked, passed the channel and channel member as arguments
+ // - dropdown_text - string or React element shown for the dropdown button description
+ // - tooltip_text - string shown for tooltip appear on hover
+ // Returns an unique identifier
+ // Minimum required version: 5.28
+ registerCallButtonAction(icon, action, dropdownText, tooltipText) {
+ const id = generateId();
+
+ const data = {
+ id,
+ pluginId: this.id,
+ icon: resolveReactElement(icon),
+ action,
+ dropdownText: resolveReactElement(dropdownText),
+ tooltipText,
+ };
+
+ store.dispatch({
+ type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
+ name: 'CallButton',
+ data,
+ });
+
+ store.dispatch({
+ type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
+ name: 'MobileChannelHeaderButton',
+ data,
+ });
+
+ return id;
+ }
+
// Register a component to render a custom body for posts with a specific type.
// Custom post types must be prefixed with 'custom_'.
// Custom post types can also apply for ephemeral posts.
diff --git a/sass/components/_dropdown.scss b/sass/components/_dropdown.scss
index 017383276cb9..a4a8ac475dbb 100644
--- a/sass/components/_dropdown.scss
+++ b/sass/components/_dropdown.scss
@@ -27,6 +27,24 @@
}
}
+ .call-plugin-icon {
+ vertical-align: middle;
+ text-align: center;
+ width: 20px;
+ height: 20px;
+ margin-right: 8px;
+ font-size: 1.6rem;
+ fill: v(center-channel-color-56);
+ color: v(center-channel-color-56);
+
+ svg,
+ img,
+ .icon {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
.a11y--focused {
box-shadow: none !important;
}