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}",