Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
Add call button (#5715)
Browse files Browse the repository at this point in the history
* Add Call dropdown for plugins

* Update i18n

* Remove plugings channel header call and reuse channel header plug

* Fix tooltip and i18n error

* Add minimum required version and remove unneeded react fragment

* Add call button to post

* Extract i18n

* Change icon by camera icon

* Remove header button for calls

* Fix tests and remove changes from the header button

* Fix lint

* Update i18n

* Remove unneeded divs, change CameraIcon to functional component, simplify aria label, prevent default on # link and fix typo

* Remove channel header references, fix lint and remove unused style

* Fix bug

* Change callbutton to functional component

* Fix CSS
  • Loading branch information
larkox committed Sep 8, 2020
1 parent d06568e commit adb578d
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 0 deletions.
111 changes: 111 additions & 0 deletions components/call_button/call_button.jsx
Original file line number Diff line number Diff line change
@@ -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 = (
<button
type='button'
className='style--none post-action icon icon--attachment'
onClick={() => {
if (item.action) {
item.action(props.currentChannel, props.channelMember);
}
}}
onTouchEnd={() => {
if (item.action) {
item.action(props.currentChannel, props.channelMember);
}
}}
>
{item.icon}
</button>
);
} else {
const pluginCallMethods = props.pluginCallMethods.map((item) => {
return (
<li
key={item.id}
onClick={(e) => {
e.preventDefault();
if (item.action) {
item.action(props.currentChannel, props.channelMember);
}
}}
>
<a href='#'>
<span className='call-plugin-icon'>
{item.icon}
</span>
{item.dropdownText}
</a>
</li>
);
});
bodyAction = (
<MenuWrapper>
<button
type='button'
className='style--none post-action'
>
<div
className='icon icon--attachment'
>
<CameraIcon className='d-flex'/>
</div>
</button>
<Menu
id='callOptions'
openLeft={true}
openUp={true}
ariaLabel={formatMessage({id: 'call_button.menuAriaLabel', defaultMessage: 'Call type selector'})}
customStyles={customStyles}
>
{pluginCallMethods}
</Menu>
</MenuWrapper>
);
}

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;
21 changes: 21 additions & 0 deletions components/call_button/index.js
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ exports[`components/create_post Show tutorial 1`] = `
<span
className="post-body__actions"
>
<Connect(injectIntl(CallButton)) />
<Connect(injectIntl(FileUpload))
fileCount={0}
getTarget={[Function]}
Expand Down Expand Up @@ -224,6 +225,7 @@ exports[`components/create_post should match snapshot for center textbox 1`] = `
<span
className="post-body__actions"
>
<Connect(injectIntl(CallButton)) />
<Connect(injectIntl(FileUpload))
fileCount={0}
getTarget={[Function]}
Expand Down Expand Up @@ -483,6 +485,7 @@ exports[`components/create_post should match snapshot when cannot post 1`] = `
<span
className="post-body__actions"
>
<Connect(injectIntl(CallButton)) />
<Connect(injectIntl(FileUpload))
fileCount={0}
getTarget={[Function]}
Expand Down Expand Up @@ -630,6 +633,7 @@ exports[`components/create_post should match snapshot when file upload disabled
<span
className="post-body__actions"
>
<Connect(injectIntl(CallButton)) />
<Connect(injectIntl(FileUpload))
fileCount={0}
getTarget={[Function]}
Expand Down Expand Up @@ -777,6 +781,7 @@ exports[`components/create_post should match snapshot, init 1`] = `
<span
className="post-body__actions"
>
<Connect(injectIntl(CallButton)) />
<Connect(injectIntl(FileUpload))
fileCount={0}
getTarget={[Function]}
Expand Down
9 changes: 9 additions & 0 deletions components/create_post/create_post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview';
import FileUpload from 'components/file_upload';
import CallButton from 'components/call_button';
import LocalizedIcon from 'components/localized_icon';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal';
Expand Down Expand Up @@ -1406,6 +1407,13 @@ class CreatePost extends React.PureComponent {
attachmentsDisabled = ' post-create--attachment-disabled';
}

let callButton;
if (!readOnlyChannel && !this.props.shouldShowPreview) {
callButton = (
<CallButton/>
);
}

let fileUpload;
if (!readOnlyChannel && !this.props.shouldShowPreview) {
fileUpload = (
Expand Down Expand Up @@ -1519,6 +1527,7 @@ class CreatePost extends React.PureComponent {
ref='createPostControls'
className='post-body__actions'
>
{callButton}
{fileUpload}
{emojiPicker}
<a
Expand Down
41 changes: 41 additions & 0 deletions components/widgets/icons/camera_icon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import {useIntl} from 'react-intl';

export default function CameraIcon(props) {
const intl = useIntl();
return (
<span {...props}>
<svg
width='14px'
height='10px'
viewBox='0 0 16 12'
role='img'
aria-label={intl.formatMessage({id: 'generic_icons.camera', defaultMessage: 'Camera Icon'})}
>
<g
stroke='none'
strokeWidth='1'
fill='inherit'
fillRule='evenodd'
>
<g
transform='translate(-696.000000, -34.000000)'
fillRule='nonzero'
fill='inherit'
>
<g transform='translate(-1.000000, 0.000000)'>
<g transform='translate(687.000000, 22.000000)'>
<g transform='translate(10.000000, 12.000000)'>
<path d='M15.105,1.447 L12,3 L12,1 C12,0.447 11.553,0 11,0 L1,0 C0.447,0 0,0.447 0,1 L0,11 C0,11.553 0.447,12 1,12 L11,12 C11.553,12 12,11.553 12,11 L12,9 L15.105,10.553 C15.6,10.8 16,10.553 16,10 L16,2 C16,1.447 15.6,1.2 15.105,1.447 Z M12.895,7.211 C12.612,7.07 12.306,7 12,7 L10.5,7 C10.224,7 10,7.224 10,7.5 L10,10 L2,10 L2,2 L10,2 L10,4.5 C10,4.776 10.224,5 10.5,5 L12,5 C12.306,5 12.612,4.93 12.895,4.789 L14,4.236 L14,7.763 L12.895,7.211 Z'/>
</g>
</g>
</g>
</g>
</g>
</svg>
</span>
);
}
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions plugins/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions sass/components/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit adb578d

Please sign in to comment.