diff --git a/app/components/post/post.js b/app/components/post/post.js index b4136e4e8..89e515f69 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -63,6 +63,7 @@ export default class Post extends PureComponent { skipFlaggedHeader: PropTypes.bool, skipPinnedHeader: PropTypes.bool, isCommentMention: PropTypes.bool, + location: PropTypes.string, }; static defaultProps = { @@ -255,6 +256,7 @@ export default class Post extends PureComponent { highlightPinnedOrFlagged, skipFlaggedHeader, skipPinnedHeader, + location, } = this.props; if (!post) { @@ -348,6 +350,7 @@ export default class Post extends PureComponent { isReplyPost={isReplyPost} showAddReaction={showAddReaction} showLongPost={showLongPost} + location={location} /> diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 93b019483..a9756cf4c 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -66,6 +66,7 @@ export default class PostBody extends PureComponent { isEmojiOnly: PropTypes.bool.isRequired, shouldRenderJumboEmoji: PropTypes.bool.isRequired, theme: PropTypes.object, + location: PropTypes.string, }; static defaultProps = { @@ -149,6 +150,7 @@ export default class PostBody extends PureComponent { navigator, postId, showAddReaction, + location, } = this.props; if (isSystemMessage && (!canDelete || hasBeenDeleted)) { @@ -178,6 +180,7 @@ export default class PostBody extends PureComponent { postId, managedConfig, showAddReaction, + location, }, }; diff --git a/app/components/post_list/post_list_base.js b/app/components/post_list/post_list_base.js index 33911377e..3328a7b35 100644 --- a/app/components/post_list/post_list_base.js +++ b/app/components/post_list/post_list_base.js @@ -171,6 +171,7 @@ export default class PostListBase extends PureComponent { onPostPress, renderReplies, shouldRenderReplyButton, + location, } = this.props; const {managedConfig} = this.state; @@ -190,6 +191,7 @@ export default class PostListBase extends PureComponent { onPress={onPostPress} navigator={navigator} managedConfig={managedConfig} + location={location} /> ); }; diff --git a/app/screens/post_options/__snapshots__/post_options.test.js.snap b/app/screens/post_options/__snapshots__/post_options.test.js.snap index eeb433783..569b96074 100644 --- a/app/screens/post_options/__snapshots__/post_options.test.js.snap +++ b/app/screens/post_options/__snapshots__/post_options.test.js.snap @@ -1,6 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PostOptions should match snapshot, no option for system message to user who doesn't have the permission to delete 1`] = `null`; +exports[`PostOptions should match snapshot, no option for system message to user who doesn't have the permission to delete 1`] = ` + + + + + + + + +`; exports[`PostOptions should match snapshot, showing Delete option only for system message to user who has permission to delete 1`] = ` + + + + - - { const {formatMessage} = this.context.intl; - const {canAddReaction, channelIsReadOnly, isSystemMessage, showAddReaction} = this.props; + const {canAddReaction} = this.props; - if (!isSystemMessage && showAddReaction && canAddReaction && !channelIsReadOnly) { + if (canAddReaction) { return ( { - if (this.props.isSystemMessage) { - return null; + getReplyOption = () => { + const {formatMessage} = this.context.intl; + const {canReply} = this.props; + + if (canReply) { + return ( + + ); } - const {formatMessage} = this.context.intl; + return null; + } - return ( - - ); + getCopyPermalink = () => { + const {formatMessage} = this.context.intl; + const {canCopyPermalink} = this.props; + + if (canCopyPermalink) { + return ( + + ); + } + + return null; }; getCopyText = () => { const {formatMessage} = this.context.intl; - const {isSystemMessage, managedConfig, post} = this.props; + const {canCopyText} = this.props; - if (!isSystemMessage && managedConfig.copyAndPasteProtection !== 'true' && post.message) { + if (canCopyText) { return ( { const {formatMessage} = this.context.intl; - const {canDelete, hasBeenDeleted} = this.props; + const {canDelete} = this.props; - if (canDelete && !hasBeenDeleted) { + if (canDelete) { return ( { const {formatMessage} = this.context.intl; - const {canEdit, canEditUntil, isSystemMessage} = this.props; + const {canEdit, canEditUntil} = this.props; - if (!isSystemMessage && canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) { + if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) { return ( { const {formatMessage} = this.context.intl; - const {channelIsReadOnly, isFlagged, isSystemMessage} = this.props; + const {canFlag, isFlagged} = this.props; - if (isSystemMessage || channelIsReadOnly) { + if (!canFlag) { return null; } @@ -182,9 +202,9 @@ export default class PostOptions extends PureComponent { getPinOption = () => { const {formatMessage} = this.context.intl; - const {channelIsReadOnly, isSystemMessage, post} = this.props; + const {canPin, post} = this.props; - if (isSystemMessage || channelIsReadOnly) { + if (!canPin) { return null; } @@ -212,51 +232,37 @@ export default class PostOptions extends PureComponent { getMyPostOptions = () => { const actions = [ this.getEditOption(), + this.getReplyOption(), this.getFlagOption(), + this.getPinOption(), this.getAddReactionOption(), this.getCopyPermalink(), this.getCopyText(), + this.getDeleteOption(), ]; - const {canDelete, canPin} = this.props; - if (canPin) { - actions.splice(2, 0, this.getPinOption()); - } - if (canDelete) { - actions.push(this.getDeleteOption()); - } - return actions.filter((a) => a !== null); }; getOthersPostOptions = () => { const actions = [ + this.getReplyOption(), this.getFlagOption(), this.getAddReactionOption(), + this.getPinOption(), this.getCopyPermalink(), this.getCopyText(), this.getEditOption(), + this.getDeleteOption(), ]; - const {canDelete, canPin} = this.props; - if (canPin) { - actions.splice(2, 0, this.getPinOption()); - } - if (canDelete) { - actions.push(this.getDeleteOption()); - } - return actions.filter((a) => a !== null); }; getPostOptions = () => { const {isMyPost} = this.props; - if (isMyPost) { - return this.getMyPostOptions(); - } - - return this.getOthersPostOptions(); + return isMyPost ? this.getMyPostOptions() : this.getOthersPostOptions(); }; handleAddReaction = () => { @@ -285,6 +291,37 @@ export default class PostOptions extends PureComponent { }); }; + handleReply = () => { + const {actions, post, navigator, theme} = this.props; + const rootId = (post.root_id || post.id); + const channelId = post.channel_id; + + actions.loadThreadIfNecessary(rootId, channelId); + actions.selectPost(rootId); + + const options = { + screen: 'Thread', + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg, + }, + passProps: { + channelId, + rootId, + }, + }; + + if (Platform.OS === 'android') { + navigator.showModal(options); + } else { + navigator.push(options); + } + } + handleAddReactionToPost = (emoji) => { const {actions, post} = this.props; diff --git a/app/screens/post_options/post_options.test.js b/app/screens/post_options/post_options.test.js index 57503d8be..2cad038d2 100644 --- a/app/screens/post_options/post_options.test.js +++ b/app/screens/post_options/post_options.test.js @@ -14,6 +14,7 @@ describe('PostOptions', () => { const navigator = { showModal: jest.fn(), dismissModal: jest.fn(), + push: jest.fn(), }; const actions = { @@ -24,16 +25,22 @@ describe('PostOptions', () => { removePost: jest.fn(), unflagPost: jest.fn(), unpinPost: jest.fn(), + selectPost: jest.fn(), + loadThreadIfNecessary: jest.fn(), }; const post = { + root_id: 'root_id', id: 'post_id', message: 'message', + is_pinned: false, + channel_id: 'channel_id', }; const baseProps = { actions, canAddReaction: true, + canReply: true, canDelete: true, canPin: true, canEdit: true, @@ -52,37 +59,44 @@ describe('PostOptions', () => { theme: Preferences.THEMES.default, }; - test('should match snapshot, showing all possible options', () => { - const wrapper = shallow( - , - {context: {intl: {formatMessage: jest.fn((format) => format.defaultMessage)}}}, + function getWrapper(props = {}) { + return shallow( + , + {context: {intl: {formatMessage: ({defaultMessage}) => defaultMessage}}} ); + } + + test('should match snapshot, showing all possible options', () => { + const wrapper = getWrapper(); expect(wrapper.getElement()).toMatchSnapshot(); }); test('should match snapshot, showing Delete option only for system message to user who has permission to delete', () => { - const wrapper = shallow( - , - {context: {intl: {formatMessage: jest.fn((format) => format.defaultMessage)}}}, - ); + const wrapper = getWrapper({isSystemMessage: true}); expect(wrapper.getElement()).toMatchSnapshot(); }); test('should match snapshot, no option for system message to user who doesn\'t have the permission to delete', () => { - const wrapper = shallow( - , - {context: {intl: {formatMessage: jest.fn((format) => format.defaultMessage)}}}, - ); + const wrapper = getWrapper({isSystemMessage: true, canDelete: false}); expect(wrapper.getElement()).toMatchSnapshot(); }); + + test('should load thread', () => { + const wrapper = getWrapper(); + + wrapper.findWhere((node) => node.key() === 'reply').simulate('press'); + expect(actions.loadThreadIfNecessary).toBeCalled(); + }); + + test('should not show reply option', () => { + const wrapper = getWrapper({canReply: false}); + + expect(wrapper.findWhere((node) => node.key() === 'reply')).toMatchObject({}); + }); }); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 81da5d375..019e3824a 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -339,6 +339,7 @@ "mobile.post_info.flag": "Flag", "mobile.post_info.unflag": "Unflag", "mobile.post_info.pin": "Pin to Channel", + "mobile.post_info.reply": "Reply", "mobile.post_info.unpin": "Unpin from Channel", "mobile.post_pre_header.flagged": "Flagged", "mobile.post_pre_header.pinned_flagged": "Pinned and Flagged", diff --git a/assets/base/images/post_menu/reply.png b/assets/base/images/post_menu/reply.png new file mode 100644 index 000000000..cbb8f6f5a Binary files /dev/null and b/assets/base/images/post_menu/reply.png differ diff --git a/assets/base/images/post_menu/reply@2x.png b/assets/base/images/post_menu/reply@2x.png new file mode 100644 index 000000000..57234c194 Binary files /dev/null and b/assets/base/images/post_menu/reply@2x.png differ diff --git a/assets/base/images/post_menu/reply@3x.png b/assets/base/images/post_menu/reply@3x.png new file mode 100644 index 000000000..306e7de33 Binary files /dev/null and b/assets/base/images/post_menu/reply@3x.png differ