diff --git a/app/components/reactions/index.js b/app/components/reactions/index.js index 2ea6fe1a57..62dc1914dd 100644 --- a/app/components/reactions/index.js +++ b/app/components/reactions/index.js @@ -14,6 +14,7 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getChannel, isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channels'; import {addReaction} from 'app/actions/views/emoji'; +import {MAX_ALLOWED_REACTIONS} from 'app/constants/emoji'; import Reactions from './reactions'; @@ -27,17 +28,23 @@ function makeMapStateToProps() { const channelIsArchived = channel.delete_at !== 0; const channelIsReadOnly = isChannelReadOnlyById(state, channelId); + const currentUserId = getCurrentUserId(state); + const reactions = getReactionsForPostSelector(state, ownProps.postId); + let canAddReaction = true; let canRemoveReaction = true; + let canAddMoreReactions = true; if (channelIsArchived || channelIsReadOnly) { canAddReaction = false; canRemoveReaction = false; + canAddMoreReactions = false; } else if (hasNewPermissions(state)) { canAddReaction = haveIChannelPermission(state, { team: teamId, channel: channelId, permission: Permissions.ADD_REACTION, }); + canAddMoreReactions = Object.values(reactions).length < MAX_ALLOWED_REACTIONS; canRemoveReaction = haveIChannelPermission(state, { team: teamId, channel: channelId, @@ -45,14 +52,12 @@ function makeMapStateToProps() { }); } - const currentUserId = getCurrentUserId(state); - const reactions = getReactionsForPostSelector(state, ownProps.postId); - return { currentUserId, reactions, theme: getTheme(state), canAddReaction, + canAddMoreReactions, canRemoveReaction, }; }; diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index 65edb8ffc0..f144bec1a4 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -5,7 +5,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Image, - ScrollView, + View, } from 'react-native'; import {intlShape} from 'react-intl'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; @@ -26,13 +26,14 @@ export default class Reactions extends PureComponent { getReactionsForPost: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, }).isRequired, + canAddReaction: PropTypes.bool, + canAddMoreReactions: PropTypes.bool, + canRemoveReaction: PropTypes.bool.isRequired, currentUserId: PropTypes.string.isRequired, position: PropTypes.oneOf(['right', 'left']), postId: PropTypes.string.isRequired, reactions: PropTypes.object, theme: PropTypes.object.isRequired, - canAddReaction: PropTypes.bool, - canRemoveReaction: PropTypes.bool.isRequired, }; static defaultProps = { @@ -132,7 +133,7 @@ export default class Reactions extends PureComponent { }; render() { - const {position, reactions, canAddReaction} = this.props; + const {position, reactions, canAddMoreReactions} = this.props; const styles = getStyleSheet(this.props.theme); if (!reactions) { @@ -140,7 +141,7 @@ export default class Reactions extends PureComponent { } let addMoreReactions = null; - if (canAddReaction) { + if (canAddMoreReactions) { addMoreReactions = ( + {reactionElements} - + ); } } @@ -207,5 +203,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { paddingHorizontal: 6, width: 40, }, + reactionsContainer: { + flex: 1, + flexDirection: 'row', + flexWrap: 'wrap', + alignContent: 'flex-start', + }, }; }); diff --git a/app/constants/emoji.js b/app/constants/emoji.js index e3742cfa77..b0526efa9c 100644 --- a/app/constants/emoji.js +++ b/app/constants/emoji.js @@ -2,3 +2,4 @@ // See LICENSE.txt for license information. export const ALL_EMOJIS = 'all_emojis'; +export const MAX_ALLOWED_REACTIONS = 40; \ No newline at end of file diff --git a/app/screens/post_options/index.js b/app/screens/post_options/index.js index f326b45e5e..65bc689929 100644 --- a/app/screens/post_options/index.js +++ b/app/screens/post_options/index.js @@ -13,6 +13,7 @@ import { removePost, } from 'mattermost-redux/actions/posts'; import {General, Permissions} from 'mattermost-redux/constants'; +import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; import {getChannel, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; @@ -21,94 +22,103 @@ import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles' import {getCurrentTeamId, getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams'; import {canEditPost} from 'mattermost-redux/utils/post_utils'; +import {MAX_ALLOWED_REACTIONS} from 'app/constants/emoji'; import {THREAD} from 'app/constants/screen'; import {addReaction} from 'app/actions/views/emoji'; import {getDimensions, isLandscape} from 'app/selectors/device'; import PostOptions from './post_options'; -export function mapStateToProps(state, ownProps) { - const post = ownProps.post; - const channel = getChannel(state, post.channel_id) || {}; - const config = getConfig(state); - const license = getLicense(state); - const currentUserId = getCurrentUserId(state); - const currentTeamId = getCurrentTeamId(state); - const currentChannelId = getCurrentChannelId(state); +export function makeMapStateToProps() { + const getReactionsForPostSelector = makeGetReactionsForPost(); - const channelIsArchived = channel.delete_at !== 0; + return (state, ownProps) => { + const post = ownProps.post; + const channel = getChannel(state, post.channel_id) || {}; + const config = getConfig(state); + const license = getLicense(state); + const currentUserId = getCurrentUserId(state); + const currentTeamId = getCurrentTeamId(state); + const currentChannelId = getCurrentChannelId(state); + const reactions = getReactionsForPostSelector(state, post.id); + const channelIsArchived = channel.delete_at !== 0; - let canAddReaction = true; - let canReply = true; - let canCopyPermalink = true; - let canCopyText = false; - let canEdit = false; - let canEditUntil = -1; - let {canDelete} = ownProps; - let canFlag = true; - let canPin = true; + let canAddReaction = true; + let canReply = true; + let canCopyPermalink = true; + let canCopyText = false; + let canEdit = false; + let canEditUntil = -1; + let {canDelete} = ownProps; + let canFlag = true; + let canPin = true; - if (hasNewPermissions(state)) { - canAddReaction = haveIChannelPermission(state, { - team: currentTeamId, - channel: post.channel_id, - permission: Permissions.ADD_REACTION, - }); - } - - if (ownProps.location === THREAD) { - canReply = false; - } - - if (channelIsArchived || ownProps.channelIsReadOnly) { - canAddReaction = false; - canReply = false; - canDelete = false; - canPin = false; - } else { - canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); - if (canEdit && license.IsLicensed === 'true' && - (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1')) - ) { - canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000); + if (hasNewPermissions(state)) { + canAddReaction = haveIChannelPermission(state, { + team: currentTeamId, + channel: post.channel_id, + permission: Permissions.ADD_REACTION, + }); } - } - if (ownProps.isSystemMessage) { - canAddReaction = false; - canReply = false; - canCopyPermalink = false; - canEdit = false; - canPin = false; - canFlag = false; - } - if (ownProps.hasBeenDeleted) { - canDelete = false; - } + if (ownProps.location === THREAD) { + canReply = false; + } - if (!ownProps.showAddReaction) { - canAddReaction = false; - } + if (channelIsArchived || ownProps.channelIsReadOnly) { + canAddReaction = false; + canReply = false; + canDelete = false; + canPin = false; + } else { + canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); + if (canEdit && license.IsLicensed === 'true' && + (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1')) + ) { + canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000); + } + } - if (!ownProps.isSystemMessage && ownProps.managedConfig?.copyAndPasteProtection !== 'true' && post.message) { - canCopyText = true; - } + if (ownProps.isSystemMessage) { + canAddReaction = false; + canReply = false; + canCopyPermalink = false; + canEdit = false; + canPin = false; + canFlag = false; + } + if (ownProps.hasBeenDeleted) { + canDelete = false; + } - return { - ...getDimensions(state), - canAddReaction, - canReply, - canCopyPermalink, - canCopyText, - canEdit, - canEditUntil, - canDelete, - canFlag, - canPin, - currentTeamUrl: getCurrentTeamUrl(state), - isMyPost: currentUserId === post.user_id, - theme: getTheme(state), - isLandscape: isLandscape(state), + if (!ownProps.showAddReaction) { + canAddReaction = false; + } + + if (!ownProps.isSystemMessage && ownProps.managedConfig?.copyAndPasteProtection !== 'true' && post.message) { + canCopyText = true; + } + + if (reactions && Object.values(reactions).length >= MAX_ALLOWED_REACTIONS) { + canAddReaction = false; + } + + return { + ...getDimensions(state), + canAddReaction, + canReply, + canCopyPermalink, + canCopyText, + canEdit, + canEditUntil, + canDelete, + canFlag, + canPin, + currentTeamUrl: getCurrentTeamUrl(state), + isMyPost: currentUserId === post.user_id, + theme: getTheme(state), + isLandscape: isLandscape(state), + }; }; } @@ -126,4 +136,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(PostOptions); +export default connect(makeMapStateToProps, mapDispatchToProps)(PostOptions); diff --git a/app/screens/post_options/index.test.js b/app/screens/post_options/index.test.js index b3b8cff04b..d07bd11313 100644 --- a/app/screens/post_options/index.test.js +++ b/app/screens/post_options/index.test.js @@ -1,6 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {mapStateToProps} from './index'; +import {makeMapStateToProps} from './index'; import * as channelSelectors from 'mattermost-redux/selectors/entities/channels'; import * as generalSelectors from 'mattermost-redux/selectors/entities/general'; @@ -24,10 +24,24 @@ deviceSelectors.getDimensions = jest.fn(); deviceSelectors.isLandscape = jest.fn(); preferencesSelectors.getTheme = jest.fn(); -describe('mapStateToProps', () => { - const baseState = {}; +describe('makeMapStateToProps', () => { + const baseState = { + entities: { + posts: { + posts: { + post_id: {}, + }, + reactions: { + post_id: {}, + }, + }, + }, + }; + const baseOwnProps = { - post: {}, + post: { + id: 'post_id', + }, }; test('canFlag is false for system messages', () => { @@ -36,6 +50,7 @@ describe('mapStateToProps', () => { isSystemMessage: true, }; + const mapStateToProps = makeMapStateToProps(); const props = mapStateToProps(baseState, ownProps); expect(props.canFlag).toBe(false); }); @@ -46,7 +61,8 @@ describe('mapStateToProps', () => { isSystemMessage: false, }; + const mapStateToProps = makeMapStateToProps(); const props = mapStateToProps(baseState, ownProps); expect(props.canFlag).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/package-lock.json b/package-lock.json index 9a885108ce..ee528d3fac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4673,7 +4673,7 @@ "dependencies": { "json5": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }