MM-17838 Wrap reactions when they exceed screen size (#3144)

* MM-17838 Wrap reactions when they exceed screen size

* Hide add reaction button after 40 reactions

* Hide add reaction button after 40 reactions

* Fix tests

* Fix crash when post has no reactions and convert to factory

* Create constant and use a separate prop for allowing more reactions
This commit is contained in:
Matheus Cardoso
2019-11-18 22:10:38 -03:00
committed by Elias Nahum
parent 07ee9f0cc9
commit 3bd3e56025
6 changed files with 130 additions and 96 deletions

View File

@@ -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,
};
};

View File

@@ -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 = (
<TouchableWithFeedback
key='addReaction'
@@ -173,14 +174,9 @@ export default class Reactions extends PureComponent {
}
return (
<ScrollView
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode='never'
keyboardShouldPersistTaps={'always'}
>
<View style={styles.reactionsContainer}>
{reactionElements}
</ScrollView>
</View>
);
}
}
@@ -207,5 +203,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingHorizontal: 6,
width: 40,
},
reactionsContainer: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'flex-start',
},
};
});

View File

@@ -2,3 +2,4 @@
// See LICENSE.txt for license information.
export const ALL_EMOJIS = 'all_emojis';
export const MAX_ALLOWED_REACTIONS = 40;

View File

@@ -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);

View File

@@ -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);
});
});
});

2
package-lock.json generated
View File

@@ -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
}