diff --git a/app/actions/views/post.js b/app/actions/views/post.js
index 732f8da9d..2427c4dc2 100644
--- a/app/actions/views/post.js
+++ b/app/actions/views/post.js
@@ -4,6 +4,8 @@
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
+import {ViewTypes} from 'app/constants';
+
import {generateId} from 'app/utils/file';
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
@@ -37,3 +39,14 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
});
};
}
+
+export function setMenuActionSelector(dataSource, onSelect, options) {
+ return {
+ type: ViewTypes.SELECTED_ACTION_MENU,
+ data: {
+ dataSource,
+ onSelect,
+ options,
+ },
+ };
+}
diff --git a/app/components/custom_list/channel_list_row/channel_list_row.js b/app/components/custom_list/channel_list_row/channel_list_row.js
index a4eba85fe..7d2ef8848 100644
--- a/app/components/custom_list/channel_list_row/channel_list_row.js
+++ b/app/components/custom_list/channel_list_row/channel_list_row.js
@@ -21,7 +21,7 @@ export default class ChannelListRow extends React.PureComponent {
};
onPress = () => {
- this.props.onPress(this.props.id);
+ this.props.onPress(this.props.id, this.props.item);
};
render() {
diff --git a/app/components/custom_list/custom_list_row.js b/app/components/custom_list/custom_list_row.js
index 08ec9fd03..494349dc3 100644
--- a/app/components/custom_list/custom_list_row.js
+++ b/app/components/custom_list/custom_list_row.js
@@ -20,6 +20,7 @@ export default class CustomListRow extends React.PureComponent {
selectable: PropTypes.bool,
selected: PropTypes.bool,
children: CustomPropTypes.Children,
+ item: PropTypes.object,
};
static defaultProps = {
diff --git a/app/components/custom_list/option_list_row/index.js b/app/components/custom_list/option_list_row/index.js
new file mode 100644
index 000000000..5df0d18f8
--- /dev/null
+++ b/app/components/custom_list/option_list_row/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import OptionListRow from './option_list_row';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(OptionListRow);
diff --git a/app/components/custom_list/option_list_row/option_list_row.js b/app/components/custom_list/option_list_row/option_list_row.js
new file mode 100644
index 000000000..48dfc17fa
--- /dev/null
+++ b/app/components/custom_list/option_list_row/option_list_row.js
@@ -0,0 +1,79 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import {
+ Text,
+ View,
+} from 'react-native';
+import {makeStyleSheetFromTheme} from 'app/utils/theme';
+
+import CustomListRow from 'app/components/custom_list/custom_list_row';
+
+export default class OptionListRow extends React.PureComponent {
+ static propTypes = {
+ id: PropTypes.string,
+ theme: PropTypes.object.isRequired,
+ ...CustomListRow.propTypes,
+ };
+
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ onPress = () => {
+ if (this.props.onPress) {
+ this.props.onPress(this.props.id, this.props.item);
+ }
+ };
+
+ render() {
+ const {
+ enabled,
+ selectable,
+ selected,
+ theme,
+ item,
+ } = this.props;
+
+ const {text, value} = item;
+ const style = getStyleFromTheme(theme);
+
+ return (
+
+
+
+
+ {text}
+
+
+
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flexDirection: 'row',
+ height: 65,
+ paddingHorizontal: 15,
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ },
+ optionText: {
+ fontSize: 15,
+ color: theme.centerChannelColor,
+ },
+ };
+});
diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js
index f98784c67..80e1b7c5d 100644
--- a/app/components/custom_list/user_list_row/user_list_row.js
+++ b/app/components/custom_list/user_list_row/user_list_row.js
@@ -29,7 +29,7 @@ export default class UserListRow extends React.PureComponent {
onPress = () => {
if (this.props.onPress) {
- this.props.onPress(this.props.id);
+ this.props.onPress(this.props.id, this.props.item);
}
};
diff --git a/app/components/message_attachments/interactive_action/interactive_action.js b/app/components/message_attachments/action_button/action_button.js
similarity index 96%
rename from app/components/message_attachments/interactive_action/interactive_action.js
rename to app/components/message_attachments/action_button/action_button.js
index 0a3f33bde..52facdfc1 100644
--- a/app/components/message_attachments/interactive_action/interactive_action.js
+++ b/app/components/message_attachments/action_button/action_button.js
@@ -9,7 +9,7 @@ import Button from 'react-native-button';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
-export default class InteractiveAction extends PureComponent {
+export default class ActionButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
doPostAction: PropTypes.func.isRequired,
diff --git a/app/components/message_attachments/interactive_action/index.js b/app/components/message_attachments/action_button/index.js
similarity index 80%
rename from app/components/message_attachments/interactive_action/index.js
rename to app/components/message_attachments/action_button/index.js
index 33fc531ce..7b87164f8 100644
--- a/app/components/message_attachments/interactive_action/index.js
+++ b/app/components/message_attachments/action_button/index.js
@@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import InteractiveAction from './interactive_action';
+import ActionButton from './action_button';
function mapStateToProps(state) {
return {
@@ -23,4 +23,4 @@ function mapDispatchToProps(dispatch) {
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(InteractiveAction);
+export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
diff --git a/app/components/message_attachments/action_menu/action_menu.js b/app/components/message_attachments/action_menu/action_menu.js
new file mode 100644
index 000000000..24a488520
--- /dev/null
+++ b/app/components/message_attachments/action_menu/action_menu.js
@@ -0,0 +1,190 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {PureComponent} from 'react';
+import {Text, View} from 'react-native';
+import PropTypes from 'prop-types';
+import {intlShape} from 'react-intl';
+import Icon from 'react-native-vector-icons/FontAwesome';
+
+import FormattedText from 'app/components/formatted_text';
+import {preventDoubleTap} from 'app/utils/tap';
+import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
+import {ViewTypes} from 'app/constants';
+
+export default class ActionMenu extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ doPostAction: PropTypes.func.isRequired,
+ setMenuActionSelector: PropTypes.func.isRequired,
+ }).isRequired,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ dataSource: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.object),
+ postId: PropTypes.string.isRequired,
+ theme: PropTypes.object.isRequired,
+ navigator: PropTypes.object,
+ };
+
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selectedText: null,
+ };
+ }
+
+ handleSelect = (selected) => {
+ if (!selected) {
+ return;
+ }
+
+ const {dataSource, actions, postId, id} = this.props;
+
+ let selectedText;
+ let selectedValue;
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ selectedText = selected.username;
+ selectedValue = selected.id;
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ selectedText = selected.display_name;
+ selectedValue = selected.id;
+ } else {
+ selectedText = selected.text;
+ selectedValue = selected.value;
+ }
+
+ this.setState({selectedText});
+
+ actions.doPostAction(postId, id, selectedValue);
+ }
+
+ goToMenuActionSelector = preventDoubleTap(() => {
+ const {intl} = this.context;
+ const {navigator, theme, actions, dataSource, options, name} = this.props;
+
+ actions.setMenuActionSelector(dataSource, this.handleSelect, options);
+
+ navigator.push({
+ backButtonTitle: '',
+ screen: 'MenuActionSelector',
+ title: name || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
+ animated: true,
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg,
+ },
+ });
+ });
+
+ render() {
+ const {intl} = this.context;
+ const {name, theme, id} = this.props;
+ const {selectedText} = this.state;
+ const style = getStyleSheet(theme);
+
+ let text = name || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
+ let selectedStyle = style.dropdownPlaceholder;
+ let submitted;
+ if (selectedText) {
+ text = selectedText;
+ selectedStyle = style.dropdownSelected;
+ submitted = (
+
+
+
+
+ );
+ } else {
+ submitted = ;
+ }
+
+ return (
+
+
+
+ {text}
+
+
+
+ {submitted}
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ width: '100%',
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ input: {
+ flex: 1,
+ position: 'relative',
+ borderWidth: 1,
+ borderRadius: 5,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.1),
+ backgroundColor: changeOpacity(theme.centerChannelBg, 0.9),
+ marginBottom: 2,
+ marginRight: 8,
+ marginTop: 10,
+ paddingLeft: 10,
+ paddingRight: 30,
+ paddingVertical: 7,
+ },
+ dropdownPlaceholder: {
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ dropdownSelected: {
+ color: theme.centerChannelColor,
+ },
+ icon: {
+ position: 'absolute',
+ top: 10,
+ right: 12,
+ },
+ blankSubmittedContainer: {
+ width: 80,
+ height: 23,
+ },
+ submittedContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 10,
+ marginBottom: 2,
+ },
+ submittedText: {
+ marginLeft: 5,
+ color: '#287B39',
+ },
+ };
+});
diff --git a/app/components/message_attachments/action_menu/index.js b/app/components/message_attachments/action_menu/index.js
new file mode 100644
index 000000000..4c3c22c01
--- /dev/null
+++ b/app/components/message_attachments/action_menu/index.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {doPostAction} from 'mattermost-redux/actions/posts';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import {setMenuActionSelector} from 'app/actions/views/post';
+
+import ActionMenu from './action_menu';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state),
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ doPostAction,
+ setMenuActionSelector,
+ }, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ActionMenu);
diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js
index 2ccd9f1a0..a73e1e8d2 100644
--- a/app/components/message_attachments/message_attachment.js
+++ b/app/components/message_attachments/message_attachment.js
@@ -22,7 +22,8 @@ import ImageCacheManager from 'app/utils/image_cache_manager';
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
-import InteractiveAction from './interactive_action';
+import ActionButton from './action_button';
+import ActionMenu from './action_menu';
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
const VIEWPORT_IMAGE_OFFSET = 32;
@@ -70,32 +71,51 @@ export default class MessageAttachment extends PureComponent {
}
getActionView = (style) => {
- const {attachment, postId} = this.props;
+ const {attachment, postId, navigator} = this.props;
const {actions} = attachment;
if (!actions || !actions.length) {
return null;
}
- const buttons = [];
+ const content = [];
actions.forEach((action) => {
if (!action.id || !action.name) {
return;
}
- buttons.push(
-
- );
+
+ switch (action.type) {
+ case 'select':
+ content.push(
+
+ );
+ break;
+ case 'button':
+ default:
+ content.push(
+
+ );
+ break;
+ }
});
return (
- {buttons}
+ {content}
);
};
diff --git a/app/constants/view.js b/app/constants/view.js
index 01f9b4aed..89515eef0 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -71,6 +71,8 @@ const ViewTypes = keyMirror({
SET_DEEP_LINK_URL: null,
SET_PROFILE_IMAGE_URI: null,
+
+ SELECTED_ACTION_MENU: null,
});
export default {
@@ -88,4 +90,6 @@ export default {
IOSX_TOP_PORTRAIT: 88,
STATUS_BAR_HEIGHT: 20,
PROFILE_PICTURE_SIZE: 32,
+ DATA_SOURCE_USERS: 'users',
+ DATA_SOURCE_CHANNELS: 'channels',
};
diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js
index 49185854b..9e34e5ae6 100644
--- a/app/reducers/views/index.js
+++ b/app/reducers/views/index.js
@@ -17,6 +17,7 @@ import team from './team';
import thread from './thread';
import user from './user';
import emoji from './emoji';
+import post from './post';
export default combineReducers({
announcement,
@@ -33,4 +34,5 @@ export default combineReducers({
thread,
user,
emoji,
+ post,
});
diff --git a/app/reducers/views/post.js b/app/reducers/views/post.js
new file mode 100644
index 000000000..417641c03
--- /dev/null
+++ b/app/reducers/views/post.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {combineReducers} from 'redux';
+
+import {ViewTypes} from 'app/constants';
+
+function menuAction(state = {}, action) {
+ switch (action.type) {
+ case ViewTypes.SELECTED_ACTION_MENU:
+ return action.data;
+
+ default:
+ return state;
+ }
+}
+
+export default combineReducers({
+ menuAction,
+});
diff --git a/app/screens/index.js b/app/screens/index.js
index c6deec65f..4d5ce4186 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -33,6 +33,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider);
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(require('app/screens/login_options').default), store, Provider);
Navigation.registerComponent('LongPost', () => wrapWithContextProvider(require('app/screens/long_post').default), store, Provider);
+ Navigation.registerComponent('MenuActionSelector', () => wrapWithContextProvider(require('app/screens/menu_action_selector').default), store, Provider);
Navigation.registerComponent('MFA', () => wrapWithContextProvider(require('app/screens/mfa').default), store, Provider);
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(require('app/screens/more_channels').default), store, Provider);
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(require('app/screens/more_dms').default), store, Provider);
diff --git a/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap b/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap
new file mode 100644
index 000000000..857cf952e
--- /dev/null
+++ b/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap
@@ -0,0 +1,1243 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MenuActionSelector should match snapshot for channels 1`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
+
+exports[`MenuActionSelector should match snapshot for channels 2`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
+
+exports[`MenuActionSelector should match snapshot for explicit options 1`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "text": "text",
+ "value": "value",
+ },
+ ],
+ "dataSource": null,
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "text": "text",
+ "value": "value",
+ },
+ ],
+ "dataSource": null,
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
+
+exports[`MenuActionSelector should match snapshot for searching 1`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "display_name": "display_name",
+ "id": "id",
+ "name": "name",
+ },
+ Object {
+ "display_name": "display_name2",
+ "id": "id2",
+ "name": "name2",
+ },
+ ],
+ "dataSource": "channels",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
+
+exports[`MenuActionSelector should match snapshot for users 1`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "id": "id",
+ "username": "username",
+ },
+ Object {
+ "id": "id2",
+ "username": "username2",
+ },
+ ],
+ "dataSource": "users",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "id": "id",
+ "username": "username",
+ },
+ Object {
+ "id": "id2",
+ "username": "username2",
+ },
+ ],
+ "dataSource": "users",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
+
+exports[`MenuActionSelector should match snapshot for users 2`] = `
+ShallowWrapper {
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateError": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "id": "id",
+ "username": "username",
+ },
+ Object {
+ "id": "id2",
+ "username": "username2",
+ },
+ ],
+ "dataSource": "users",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "actions": Object {
+ "getChannels": [Function],
+ "getProfiles": [Function],
+ "searchChannels": [Function],
+ "searchProfiles": [Function],
+ },
+ "currentTeamId": "someId",
+ "data": Array [
+ Object {
+ "id": "id",
+ "username": "username",
+ },
+ Object {
+ "id": "id2",
+ "username": "username2",
+ },
+ ],
+ "dataSource": "users",
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ "navigator": Object {
+ "setOnNavigatorEvent": [MockFunction],
+ },
+ "onSelect": [MockFunction],
+ "theme": Object {},
+ },
+ "ref": null,
+ "rendered": null,
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ "lifecycles": Object {
+ "componentDidUpdate": Object {
+ "onSetState": true,
+ },
+ "getDerivedStateFromProps": true,
+ "getSnapshotBeforeUpdate": true,
+ "setState": Object {
+ "skipsComponentDidUpdateOnNullish": true,
+ },
+ },
+ },
+ },
+ "attachTo": undefined,
+ "context": Object {
+ "intl": Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {},
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "textComponent": "span",
+ "timeZone": null,
+ },
+ },
+ "hydrateIn": undefined,
+ },
+}
+`;
diff --git a/app/screens/menu_action_selector/index.js b/app/screens/menu_action_selector/index.js
new file mode 100644
index 000000000..112d61320
--- /dev/null
+++ b/app/screens/menu_action_selector/index.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import {getProfiles} from 'mattermost-redux/selectors/entities/users';
+import {getProfiles as getProfilesAction, searchProfiles} from 'mattermost-redux/actions/users';
+import {getChannels, searchChannels} from 'mattermost-redux/actions/channels';
+
+import {ViewTypes} from 'app/constants';
+
+import MenuActionSelector from './menu_action_selector';
+
+function mapStateToProps(state) {
+ const menuAction = state.views.post.menuAction || {};
+
+ let data;
+ let loadMoreRequestStatus;
+ let searchRequestStatus;
+ if (menuAction.dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ data = getProfiles(state);
+ loadMoreRequestStatus = state.requests.users.getProfiles.status;
+ searchRequestStatus = state.requests.users.searchProfiles.status;
+ } else if (menuAction.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ data = getChannelsInCurrentTeam(state);
+ loadMoreRequestStatus = state.requests.channels.getChannels.status;
+ searchRequestStatus = state.requests.channels.getChannels.status;
+ } else {
+ data = menuAction.options || [];
+ }
+
+ return {
+ data,
+ dataSource: menuAction.dataSource,
+ onSelect: menuAction.onSelect,
+ theme: getTheme(state),
+ currentTeamId: getCurrentTeamId(state),
+ loadMoreRequestStatus,
+ searchRequestStatus,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ getProfiles: getProfilesAction,
+ getChannels,
+ searchProfiles,
+ searchChannels,
+ }, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MenuActionSelector);
diff --git a/app/screens/menu_action_selector/menu_action_selector.js b/app/screens/menu_action_selector/menu_action_selector.js
new file mode 100644
index 000000000..7dd3cef50
--- /dev/null
+++ b/app/screens/menu_action_selector/menu_action_selector.js
@@ -0,0 +1,286 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {injectIntl, intlShape} from 'react-intl';
+import {
+ InteractionManager,
+ Platform,
+ View,
+} from 'react-native';
+
+import CustomList from 'app/components/custom_list';
+import UserListRow from 'app/components/custom_list/user_list_row';
+import ChannelListRow from 'app/components/custom_list/channel_list_row';
+import OptionListRow from 'app/components/custom_list/option_list_row';
+import SearchBar from 'app/components/search_bar';
+import StatusBar from 'app/components/status_bar';
+import {loadingText} from 'app/utils/member_list';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
+import {ViewTypes} from 'app/constants';
+
+import {General, RequestStatus} from 'mattermost-redux/constants';
+import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
+import {filterChannelsMatchingTerm} from 'mattermost-redux/utils/channel_utils';
+import {memoizeResult} from 'mattermost-redux/utils/helpers';
+
+class MenuActionSelector extends PureComponent {
+ static propTypes = {
+ intl: intlShape.isRequired,
+ theme: PropTypes.object.isRequired,
+ navigator: PropTypes.object,
+ data: PropTypes.arrayOf(PropTypes.object),
+ dataSource: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ currentTeamId: PropTypes.string.isRequired,
+ loadMoreRequestStatus: PropTypes.string,
+ searchRequestStatus: PropTypes.string,
+ actions: PropTypes.shape({
+ getProfiles: PropTypes.func.isRequired,
+ getChannels: PropTypes.func.isRequired,
+ searchProfiles: PropTypes.func.isRequired,
+ searchChannels: PropTypes.func.isRequired,
+ }),
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.searchTimeoutId = 0;
+
+ let data = [];
+ if (!props.dataSource) {
+ data = props.data;
+ }
+
+ const needsLoading = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS;
+
+ this.state = {
+ next: needsLoading,
+ page: 0,
+ data,
+ searching: false,
+ showNoResults: false,
+ term: '',
+ isLoading: needsLoading,
+ prevLoadMoreRequestStatus: RequestStatus.STARTED,
+ };
+
+ props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ InteractionManager.runAfterInteractions(() => {
+ if (this.props.dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ this.props.actions.getProfiles().then(() => this.setState({isLoading: false}));
+ } else if (this.props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ this.props.actions.getChannels(this.props.currentTeamId).then(() => this.setState({isLoading: false}));
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.theme !== prevProps.theme) {
+ setNavigatorStyles(this.props.navigator, this.props.theme);
+ }
+ }
+
+ cancelSearch = () => {
+ this.setState({
+ searching: false,
+ isLoading: false,
+ term: '',
+ page: 0,
+ data: filterPageData(this.props.data, 0),
+ });
+ };
+
+ close = () => {
+ this.props.navigator.pop({animated: true});
+ };
+
+ handleRowSelect = (id, selected) => {
+ this.props.onSelect(selected);
+ this.close();
+ };
+
+ loadMore = async () => {
+ const {actions, loadMoreRequestStatus, currentTeamId, dataSource} = this.props;
+ const {next, searching} = this.state;
+ let {page} = this.state;
+
+ if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
+ page = page + 1;
+
+ let results;
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ results = await actions.getProfiles(page, General.PROFILE_CHUNK_SIZE);
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ results = await actions.getChannels(currentTeamId, page, General.PROFILE_CHUNK_SIZE);
+ } else {
+ return;
+ }
+
+ if (!this.mounted) {
+ return;
+ }
+
+ if (results.data && results.data.length) {
+ this.setState({
+ isLoading: false,
+ page,
+ });
+ } else {
+ this.setState({
+ isLoading: false,
+ next: false,
+ });
+ }
+ }
+ };
+
+ searchProfiles = (text) => {
+ const term = text;
+ const {actions, currentTeamId, dataSource} = this.props;
+
+ if (term) {
+ if (!dataSource) {
+ this.setState({data: filterSearchData(null, this.props.data, term.toLowerCase())});
+ return;
+ }
+
+ this.setState({searching: true, isLoading: true, term});
+ clearTimeout(this.searchTimeoutId);
+
+ this.searchTimeoutId = setTimeout(async () => {
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ await actions.searchProfiles(term.toLowerCase());
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ await actions.searchChannels(currentTeamId, term.toLowerCase());
+ }
+
+ if (!this.mounted) {
+ return;
+ }
+
+ this.setState({isLoading: false});
+ }, General.SEARCH_TIMEOUT_MILLISECONDS);
+ } else {
+ this.cancelSearch();
+ }
+ };
+
+ render() {
+ const {intl, data, theme, dataSource} = this.props;
+ const {searching, term, page} = this.state;
+ const {formatMessage} = intl;
+ const style = getStyleFromTheme(theme);
+ const more = searching ? () => true : this.loadMore;
+
+ const searchBarInput = {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
+ color: theme.centerChannelColor,
+ fontSize: 15,
+ ...Platform.select({
+ android: {
+ marginBottom: -5,
+ },
+ }),
+ };
+
+ let rowComponent;
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ rowComponent = UserListRow;
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ rowComponent = ChannelListRow;
+ } else {
+ rowComponent = OptionListRow;
+ }
+
+ let filteredData;
+ if (searching) {
+ filteredData = filterSearchData(dataSource, data, term);
+ } else {
+ filteredData = filterPageData(data, page);
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ },
+ searchContainer: {
+ marginVertical: 5,
+ },
+ };
+});
+
+const filterPageData = memoizeResult((data, page) => {
+ return data.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE);
+});
+
+const filterSearchData = memoizeResult((dataSource, data, term) => {
+ if (!data) {
+ return [];
+ }
+
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ return filterProfilesMatchingTerm(data, term);
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ return filterChannelsMatchingTerm(data, term);
+ }
+
+ return data.filter((option) => option.text && option.text.toLowerCase().startsWith(term));
+});
+
+export default injectIntl(MenuActionSelector);
diff --git a/app/screens/menu_action_selector/menu_action_selector.test.js b/app/screens/menu_action_selector/menu_action_selector.test.js
new file mode 100644
index 000000000..1ff24d4e6
--- /dev/null
+++ b/app/screens/menu_action_selector/menu_action_selector.test.js
@@ -0,0 +1,139 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+import {shallow} from 'enzyme';
+import {IntlProvider} from 'react-intl';
+
+import MenuActionSelector from './menu_action_selector.js';
+
+jest.mock('rn-fetch-blob', () => ({
+ fs: {
+ dirs: {
+ DocumentDir: () => jest.fn(),
+ CacheDir: () => jest.fn(),
+ },
+ },
+}));
+
+jest.mock('rn-fetch-blob/fs', () => ({
+ dirs: {
+ DocumentDir: () => jest.fn(),
+ CacheDir: () => jest.fn(),
+ },
+}));
+
+const user1 = {id: 'id', username: 'username'};
+const user2 = {id: 'id2', username: 'username2'};
+
+const getProfiles = async () => {
+ return {
+ data: [user1, user2],
+ error: {},
+ };
+};
+
+const searchProfiles = async () => {
+ return {
+ data: [user2],
+ error: {},
+ };
+};
+
+const channel1 = {id: 'id', name: 'name', display_name: 'display_name'};
+const channel2 = {id: 'id2', name: 'name2', display_name: 'display_name2'};
+
+const getChannels = async () => {
+ return {
+ data: [channel1, channel2],
+ error: {},
+ };
+};
+
+const searchChannels = async () => {
+ return {
+ data: [channel2],
+ error: {},
+ };
+};
+
+const intlProvider = new IntlProvider({locale: 'en'}, {});
+const {intl} = intlProvider.getChildContext();
+
+describe('MenuActionSelector', () => {
+ const actions = {
+ getProfiles,
+ getChannels,
+ searchProfiles,
+ searchChannels,
+ };
+
+ const baseProps = {
+ actions,
+ currentTeamId: 'someId',
+ navigator: {
+ setOnNavigatorEvent: jest.fn(),
+ },
+ onSelect: jest.fn(),
+ data: [{text: 'text', value: 'value'}],
+ dataSource: null,
+ theme: {},
+ };
+
+ test('should match snapshot for explicit options', async () => {
+ const wrapper = shallow(
+ ,
+ {context: {intl}},
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot for users', async () => {
+ const props = {
+ ...baseProps,
+ dataSource: 'users',
+ data: [user1, user2],
+ };
+
+ const wrapper = shallow(
+ ,
+ {context: {intl}},
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({isLoading: false});
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot for channels', async () => {
+ const props = {
+ ...baseProps,
+ dataSource: 'channels',
+ data: [channel1, channel2],
+ };
+
+ const wrapper = shallow(
+ ,
+ {context: {intl}},
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({isLoading: false});
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot for searching', async () => {
+ const props = {
+ ...baseProps,
+ dataSource: 'channels',
+ data: [channel1, channel2],
+ };
+
+ const wrapper = shallow(
+ ,
+ {context: {intl}},
+ );
+ wrapper.setState({isLoading: false, searching: true, term: 'name2'});
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 20f95b24b..3f8c1854c 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -21,6 +21,8 @@
"about.title": "About Mattermost",
"about.version": "Mattermost Version:",
"access_history.title": "Access History",
+ "mobile.action_menu.select": "Select an option",
+ "mobile.action_menu.submitted": "Submitted",
"activity_log.activeSessions": "Active Sessions",
"activity_log.browser": "Browser: {browser}",
"activity_log.firstTime": "First time active: {date}, {time}",