diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 00a107909..e03093e23 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -155,13 +155,13 @@ export function loadPostsIfNecessary(channelId) { }; } -export function loadFilesForPostIfNecessary(post) { +export function loadFilesForPostIfNecessary(postId) { return async (dispatch, getState) => { const {files} = getState().entities; - const fileIdsForPost = files.fileIdsByPostId[post.id]; + const fileIdsForPost = files.fileIdsByPostId[postId]; if (!fileIdsForPost) { - await getFilesForPost(post.id)(dispatch, getState); + await getFilesForPost(postId)(dispatch, getState); } }; } diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 0445f8f93..f00f772ee 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -16,39 +16,41 @@ export default class FileAttachmentList extends Component { static propTypes = { actions: PropTypes.object.isRequired, fetchCache: PropTypes.object.isRequired, + fileIds: PropTypes.array.isRequired, files: PropTypes.array.isRequired, hideOptionsContext: PropTypes.func.isRequired, + isFailed: PropTypes.bool, navigator: PropTypes.object, onLongPress: PropTypes.func, onPress: PropTypes.func, - post: PropTypes.object.isRequired, + postId: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, toggleSelected: PropTypes.func.isRequired, filesForPostRequest: PropTypes.object.isRequired }; componentDidMount() { - const {post} = this.props; - this.props.actions.loadFilesForPostIfNecessary(post); + const {postId} = this.props; + this.props.actions.loadFilesForPostIfNecessary(postId); } componentDidUpdate() { - const {files, filesForPostRequest, post} = this.props; + const {fileIds, files, filesForPostRequest, postId} = this.props; // Fixes an issue where files weren't loading with optimistic post - if (!files.length && post.file_ids.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) { - this.props.actions.loadFilesForPostIfNecessary(post); + if (!files.length && fileIds.length > 0 && filesForPostRequest.status !== RequestStatus.STARTED) { + this.props.actions.loadFilesForPostIfNecessary(postId); } } - goToImagePreview = (post, fileId) => { + goToImagePreview = (postId, fileId) => { this.props.navigator.showModal({ screen: 'ImagePreview', title: '', animationType: 'none', passProps: { fileId, - post + postId }, navigatorStyle: { navBarHidden: true, @@ -67,15 +69,23 @@ export default class FileAttachmentList extends Component { handlePreviewPress = (file) => { this.props.hideOptionsContext(); - preventDoubleTap(this.goToImagePreview, this, this.props.post, file.id); + preventDoubleTap(this.goToImagePreview, this, this.props.postId, file.id); + }; + + handlePressIn = () => { + this.props.toggleSelected(true); + }; + + handlePressOut = () => { + this.props.toggleSelected(false); }; render() { - const {files, post} = this.props; + const {fileIds, files, isFailed} = this.props; let fileAttachments; - if (!files.length && post.file_ids.length > 0) { - fileAttachments = post.file_ids.map((id) => ( + if (!files.length && fileIds.length > 0) { + fileAttachments = fileIds.map((id) => ( this.props.toggleSelected(true)} - onPressOut={() => this.props.toggleSelected(false)} + onPressIn={this.handlePressIn} + onPressOut={this.handlePressOut} > + {fileAttachments} ); diff --git a/app/components/file_attachment_list/index.js b/app/components/file_attachment_list/index.js index 9e7a29341..83ed7b8b4 100644 --- a/app/components/file_attachment_list/index.js +++ b/app/components/file_attachment_list/index.js @@ -17,7 +17,7 @@ function makeMapStateToProps() { return { ...ownProps, fetchCache: state.views.fetchCache, - files: getFilesForPost(state, ownProps.post), + files: getFilesForPost(state, ownProps.postId), theme: getTheme(state), filesForPostRequest: state.requests.files.getFilesForPost }; diff --git a/app/components/options_context/options_context.android.js b/app/components/options_context/options_context.android.js index 5b44c5e3c..4017e2d53 100644 --- a/app/components/options_context/options_context.android.js +++ b/app/components/options_context/options_context.android.js @@ -1,14 +1,18 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {PureComponent} from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {TouchableHighlight, View} from 'react-native'; import RNBottomSheet from 'react-native-bottom-sheet'; export default class OptionsContext extends PureComponent { static propTypes = { actions: PropTypes.array, - cancelText: PropTypes.string + cancelText: PropTypes.string, + children: PropTypes.node.isRequired, + onPress: PropTypes.func.isRequired, + toggleSelected: PropTypes.func.isRequired }; static defaultProps = { @@ -34,7 +38,28 @@ export default class OptionsContext extends PureComponent { } }; + handleHideUnderlay = () => { + this.props.toggleSelected(false); + }; + + handleShowUnderlay = () => { + this.props.toggleSelected(true); + }; + render() { - return null; + return ( + + + {this.props.children} + + + ); } } diff --git a/app/components/options_context/options_context.ios.js b/app/components/options_context/options_context.ios.js index caa1adca4..c70e88450 100644 --- a/app/components/options_context/options_context.ios.js +++ b/app/components/options_context/options_context.ios.js @@ -17,13 +17,21 @@ export default class OptionsContext extends PureComponent { actions: [] }; - hide() { - this.refs.toolTip.hideMenu(); - } + handleHide = () => { + this.props.toggleSelected(false); + }; - show() { + handleShow = () => { + this.props.toggleSelected(true); + }; + + hide = () => { + this.refs.toolTip.hideMenu(); + }; + + show = () => { this.refs.toolTip.showMenu(); - } + }; render() { return ( @@ -34,8 +42,8 @@ export default class OptionsContext extends PureComponent { longPress={true} onPress={this.props.onPress} underlayColor='transparent' - onShow={() => this.props.toggleSelected(true)} - onHide={() => this.props.toggleSelected(false)} + onShow={this.handleShow} + onHide={this.handleHide} > {this.props.children} diff --git a/app/components/post/index.js b/app/components/post/index.js index b757e9e2a..0d9c9f8d5 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -4,12 +4,9 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {createPost, deletePost, flagPost, removePost, unflagPost} from 'mattermost-redux/actions/posts'; -import {getMyPreferences, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; -import {makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/selectors/entities/users'; -import {isPostFlagged} from 'mattermost-redux/utils/post_utils'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; +import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts'; +import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {setPostTooltipVisible} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; @@ -17,29 +14,22 @@ import {getTheme} from 'app/selectors/preferences'; import Post from './post'; function makeMapStateToProps() { - const getCommentCountForPost = makeGetCommentCountForPost(); return function mapStateToProps(state, ownProps) { - const commentedOnUser = ownProps.commentedOnPost ? getUser(state, ownProps.commentedOnPost.user_id) : null; - const user = getUser(state, ownProps.post.user_id); - const myPreferences = getMyPreferences(state); - const teammateNameDisplay = getTeammateNameDisplaySetting(state); + const post = getPost(state, ownProps.post.id); + const {config, license} = state.entities.general; const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : ''; const {tooltipVisible} = state.views.channel; return { ...ownProps, + post, config, - commentCount: getCommentCountForPost(state, ownProps), - commentedOnDisplayName: displayUsername(commentedOnUser, teammateNameDisplay), currentUserId: getCurrentUserId(state), - displayName: displayUsername(user, teammateNameDisplay), - isFlagged: isPostFlagged(ownProps.post.id, myPreferences), license, roles, theme: getTheme(state), - tooltipVisible, - user + tooltipVisible }; }; } @@ -49,10 +39,8 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ createPost, deletePost, - flagPost, removePost, - setPostTooltipVisible, - unflagPost + setPostTooltipVisible }, dispatch) }; } diff --git a/app/components/post/post.js b/app/components/post/post.js index aa4f9ca8b..9711c84fa 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -5,73 +5,50 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Alert, - Image, - Platform, StyleSheet, - Text, - TouchableHighlight, - TouchableOpacity, View, ViewPropTypes } from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; -import Icon from 'react-native-vector-icons/Ionicons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import PostBody from 'app/components/post_body'; +import PostHeader from 'app/components/post_header'; +import PostProfilePicture from 'app/components/post_profile_picture'; import {NavigationTypes} from 'app/constants'; -import FileAttachmentList from 'app/components/file_attachment_list'; -import FormattedText from 'app/components/formatted_text'; -import FormattedTime from 'app/components/formatted_time'; -import MattermostIcon from 'app/components/mattermost_icon'; -import Markdown from 'app/components/markdown'; -import OptionsContext from 'app/components/options_context'; -import ProfilePicture from 'app/components/profile_picture'; -import ReplyIcon from 'app/components/reply_icon'; -import SlackAttachments from 'app/components/slack_attachments'; +import {emptyFunction} from 'app/utils/general'; import {preventDoubleTap} from 'app/utils/tap'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import webhookIcon from 'assets/images/icons/webhook.jpg'; - import {Posts} from 'mattermost-redux/constants'; import DelayedAction from 'mattermost-redux/utils/delayed_action'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {canDeletePost, canEditPost, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; -const BOT_NAME = 'BOT'; - class Post extends PureComponent { static propTypes = { + actions: PropTypes.shape({ + createPost: PropTypes.func.isRequired, + deletePost: PropTypes.func.isRequired, + removePost: PropTypes.func.isRequired, + setPostTooltipVisible: PropTypes.func.isRequired + }).isRequired, config: PropTypes.object.isRequired, - commentCount: PropTypes.number.isRequired, currentUserId: PropTypes.string.isRequired, intl: intlShape.isRequired, style: ViewPropTypes.style, post: PropTypes.object.isRequired, - user: PropTypes.object, - displayName: PropTypes.string, renderReplies: PropTypes.bool, isFirstReply: PropTypes.bool, - isFlagged: PropTypes.bool, isLastReply: PropTypes.bool, - commentedOnDisplayName: PropTypes.string, commentedOnPost: PropTypes.object, license: PropTypes.object.isRequired, navigator: PropTypes.object, roles: PropTypes.string, tooltipVisible: PropTypes.bool, theme: PropTypes.object.isRequired, - onPress: PropTypes.func, - actions: PropTypes.shape({ - createPost: PropTypes.func.isRequired, - deletePost: PropTypes.func.isRequired, - flagPost: PropTypes.func.isRequired, - removePost: PropTypes.func.isRequired, - setPostTooltipVisible: PropTypes.func.isRequired, - unflagPost: PropTypes.func.isRequired - }).isRequired + onPress: PropTypes.func }; constructor(props) { @@ -97,15 +74,15 @@ class Post extends PureComponent { this.editDisableAction.cancel(); } - goToUserProfile = (userId) => { - const {intl, navigator, theme} = this.props; + goToUserProfile = () => { + const {intl, navigator, post, theme} = this.props; navigator.push({ screen: 'UserProfile', title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), animated: true, backButtonTitle: '', passProps: { - userId + userId: post.user_id }, navigatorStyle: { navBarTextColor: theme.sidebarHeaderTextColor, @@ -224,258 +201,41 @@ class Post extends PureComponent { } }; - hideOptionsContext = () => { - if (Platform.OS === 'ios') { - this.refs.tooltip.hide(); - } - }; - onRemovePost = (post) => { const {removePost} = this.props.actions; removePost(post); }; - showOptionsContext = () => { - if (Platform.OS === 'ios') { - return this.refs.tooltip.show(); - } + renderReplyBar = () => { + const { + commentedOnPost, + isFirstReply, + isLastReply, + post, + renderReplies, + theme + } = this.props; - return this.refs.bottomSheet.show(); - }; - - renderCommentedOnMessage = (style) => { - if (!this.props.renderReplies || !this.props.commentedOnPost) { - return null; - } - - const displayName = this.props.commentedOnDisplayName; - - let name; - if (displayName) { - name = displayName; - } else { - name = ( - - ); - } - - let apostrophe; - if (displayName && displayName.slice(-1) === 's') { - apostrophe = '\''; - } else { - apostrophe = '\'s'; - } - - return ( - - ); - }; - - renderReplyBar = (style) => { - if (!this.props.renderReplies || !this.props.post.root_id) { + if (!renderReplies || !post.root_id) { return null; } + const style = getStyleSheet(theme); const replyBarStyle = [style.replyBar]; - if (this.props.isFirstReply || this.props.commentedOnPost) { + if (isFirstReply || commentedOnPost) { replyBarStyle.push(style.replyBarFirst); } - if (this.props.isLastReply) { + if (isLastReply) { replyBarStyle.push(style.replyBarLast); } return ; }; - renderFileAttachments() { - const {navigator, post} = this.props; - const fileIds = post.file_ids || []; - - let attachments; - if (fileIds.length > 0) { - attachments = ( - - ); - } - return attachments; - } - - renderSlackAttachments = (baseStyle, blockStyles, textStyles) => { - const {post, theme} = this.props; - - if (post.props) { - const {attachments} = post.props; - - if (attachments && attachments.length) { - return ( - - ); - } - } - - return null; - }; - - renderMessage = (style, messageStyle, blockStyles, textStyles, replyBar = false) => { - const {formatMessage} = this.props.intl; - const {isFlagged, post, theme, navigator} = this.props; - const {flagPost, unflagPost} = this.props.actions; - const actions = []; - - // we should check for the user roles and permissions - if (!isPostPendingOrFailed(post)) { - if (isFlagged) { - actions.push({ - text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}), - onPress: () => unflagPost(post.id) - }); - } else { - actions.push({ - text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}), - onPress: () => flagPost(post.id) - }); - } - - if (this.state.canEdit) { - actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: () => this.handlePostEdit()}); - } - - if (this.state.canDelete && post.state !== Posts.POST_DELETED) { - actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: () => this.handlePostDelete()}); - } - } - - let messageContainer; - let message; - if (post.state === Posts.POST_DELETED) { - message = ( - - ); - } else if (this.props.post.message.length) { - message = ( - - - - - - ); - } - - if (Platform.OS === 'ios') { - messageContainer = ( - - {replyBar && this.renderReplyBar(style)} - - - - {message} - {this.renderSlackAttachments(messageStyle, blockStyles, textStyles)} - {this.renderFileAttachments()} - - - {post.failed && - - - - } - - - ); - } else { - messageContainer = ( - - {replyBar && this.renderReplyBar(style)} - this.toggleSelected(false)} - onLongPress={this.showOptionsContext} - onPress={this.handlePress} - onShowUnderlay={() => this.toggleSelected(true)} - underlayColor='transparent' - style={{flex: 1, flexDirection: 'row'}} - > - - - {message} - - {this.renderSlackAttachments(messageStyle, blockStyles, textStyles)} - {this.renderFileAttachments()} - - {post.failed && - - - - } - - - - ); - } - - return messageContainer; - }; - viewUserProfile = () => { - preventDoubleTap(this.goToUserProfile, null, this.props.user.id); + preventDoubleTap(this.goToUserProfile, this); }; toggleSelected = (selected) => { @@ -485,176 +245,51 @@ class Post extends PureComponent { render() { const { - commentCount, - config, + commentedOnPost, + isLastReply, post, renderReplies, theme } = this.props; const style = getStyleSheet(theme); - const PROFILE_PICTURE_SIZE = 32; - - let profilePicture; - let displayName; - let messageStyle; - if (isSystemMessage(post)) { - profilePicture = ( - - - - ); - - displayName = ( - - ); - - messageStyle = [style.message, style.systemMessage]; - } else if (post.props && post.props.from_webhook) { - if (config.EnablePostIconOverride === 'true') { - const icon = post.props.override_icon_url ? {uri: post.props.override_icon_url} : webhookIcon; - profilePicture = ( - - - - ); - } else { - profilePicture = ( - - ); - } - - let name = this.props.displayName; - if (post.props.override_username && config.EnablePostUsernameOverride === 'true') { - name = post.props.override_username; - } - displayName = ( - - - {name} - - - {BOT_NAME} - - - ); - messageStyle = style.message; - } else { - profilePicture = ( - - - - ); - - if (this.props.displayName) { - displayName = ( - - - {this.props.displayName} - - - ); - } else { - displayName = ( - - ); - } - - messageStyle = style.message; - } - - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); - const selected = this.state && this.state.selected ? style.selected : null; - let contents; - if (this.props.commentedOnPost) { - contents = ( - - - {profilePicture} - - - - {displayName} - - - - - - - - {this.renderCommentedOnMessage(style)} - - {this.renderMessage(style, messageStyle, blockStyles, textStyles, true)} + return ( + + + + + + {!commentedOnPost && this.renderReplyBar()} + + + - ); - } else { - contents = ( - - - {profilePicture} - - - {this.renderReplyBar(style)} - - - - {displayName} - - - - - - - {(commentCount > 0 && renderReplies) && - - - {commentCount} - - } - - {this.renderMessage(style, messageStyle, blockStyles, textStyles)} - - - - ); - } - - return contents; + + ); } } @@ -676,11 +311,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { rightColumnPadding: { paddingBottom: 3 }, - postInfoContainer: { - alignItems: 'center', - flexDirection: 'row', - marginTop: 10 - }, messageContainerWithReplyBar: { flexDirection: 'row', flex: 1 @@ -704,61 +334,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { replyBarLast: { paddingBottom: 10 }, - displayName: { - color: theme.centerChannelColor, - fontSize: 15, - fontWeight: '600', - marginRight: 5, - marginBottom: 3 - }, - botContainer: { - flexDirection: 'row' - }, - bot: { - alignSelf: 'center', - backgroundColor: changeOpacity(theme.centerChannelColor, 0.15), - borderRadius: 2, - color: theme.centerChannelColor, - fontSize: 10, - fontWeight: '600', - marginRight: 5, - paddingVertical: 2, - paddingHorizontal: 4 - }, - time: { - color: theme.centerChannelColor, - fontSize: 13, - marginLeft: 5, - marginBottom: 1, - opacity: 0.5 - }, - timeContainer: { - justifyContent: 'center' - }, - commentedOn: { - color: changeOpacity(theme.centerChannelColor, 0.65), - marginBottom: 3, - lineHeight: 21 - }, - message: { - color: theme.centerChannelColor, - fontSize: 15 - }, - systemMessage: { - opacity: 0.6 - }, selected: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.1) - }, - replyIconContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center' - }, - replyText: { - fontSize: 15, - marginLeft: 3, - color: theme.linkColor } }); }); diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js new file mode 100644 index 000000000..f0ddaa9aa --- /dev/null +++ b/app/components/post_body/index.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; + +import {flagPost, unflagPost} from 'mattermost-redux/actions/posts'; +import {Posts} from 'mattermost-redux/constants'; +import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; +import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils'; + +import {getTheme} from 'app/selectors/preferences'; + +import PostBody from './post_body'; + +function mapStateToProps(state, ownProps) { + const post = getPost(state, ownProps.postId); + const myPreferences = getMyPreferences(state); + + return { + ...ownProps, + attachments: post.props && post.props.attachments, + fileIds: post.file_ids, + hasBeenDeleted: post.state === Posts.POST_DELETED, + isFailed: post.failed, + isFlagged: isPostFlagged(post.id, myPreferences), + isPending: post.id === post.pending_post_id, + isSystemMessage: isSystemMessage(post), + message: post.message, + theme: getTheme(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + flagPost, + unflagPost + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(PostBody); diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js new file mode 100644 index 000000000..8ae34006d --- /dev/null +++ b/app/components/post_body/post_body.js @@ -0,0 +1,261 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + StyleSheet, + TouchableOpacity, + View +} from 'react-native'; +import {injectIntl, intlShape} from 'react-intl'; +import Icon from 'react-native-vector-icons/Ionicons'; + +import FileAttachmentList from 'app/components/file_attachment_list'; +import FormattedText from 'app/components/formatted_text'; +import Markdown from 'app/components/markdown'; +import OptionsContext from 'app/components/options_context'; +import SlackAttachments from 'app/components/slack_attachments'; + +import {emptyFunction} from 'app/utils/general'; +import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +class PostBody extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + flagPost: PropTypes.func.isRequired, + unflagPost: PropTypes.func.isRequired + }).isRequired, + attachments: PropTypes.array, + canDelete: PropTypes.bool, + canEdit: PropTypes.bool, + fileIds: PropTypes.array, + hasBeenDeleted: PropTypes.bool, + intl: intlShape.isRequired, + isFailed: PropTypes.bool, + isFlagged: PropTypes.bool, + isPending: PropTypes.bool, + isSystemMessage: PropTypes.bool, + message: PropTypes.string, + navigator: PropTypes.object.isRequired, + onFailedPostPress: PropTypes.func, + onPostDelete: PropTypes.func, + onPostEdit: PropTypes.func, + onPress: PropTypes.func, + postId: PropTypes.string.isRequired, + renderReplyBar: PropTypes.func, + theme: PropTypes.object, + toggleSelected: PropTypes.func + }; + + static defaultProps = { + fileIds: [], + onFailedPostPress: emptyFunction, + onPostDelete: emptyFunction, + onPostEdit: emptyFunction, + onPress: emptyFunction, + renderReplyBar: emptyFunction, + toggleSelected: emptyFunction + }; + + hideOptionsContext = () => { + if (Platform.OS === 'ios') { + this.refs.options.hide(); + } + }; + + flagPost = () => { + const {actions, postId} = this.props; + actions.flagPost(postId); + }; + + unflagPost = () => { + const {actions, postId} = this.props; + actions.unflagPost(postId); + }; + + showOptionsContext = () => { + return this.refs.options.show(); + }; + + renderFileAttachments() { + const { + fileIds, + isFailed, + navigator, + onPress, + postId, + toggleSelected + } = this.props; + + let attachments; + if (fileIds.length > 0) { + attachments = ( + + ); + } + return attachments; + } + + renderSlackAttachments = (baseStyle, blockStyles, textStyles) => { + const {attachments, navigator, theme} = this.props; + + if (attachments && attachments.length) { + return ( + + ); + } + + return null; + }; + + render() { + const { + canDelete, + canEdit, + hasBeenDeleted, + isFailed, + isFlagged, + isPending, + isSystemMessage, + intl, + message, + navigator, + onFailedPostPress, + onPostDelete, + onPostEdit, + onPress, + renderReplyBar, + theme, + toggleSelected + } = this.props; + const {formatMessage} = intl; + const actions = []; + const style = getStyleSheet(theme); + const blockStyles = getMarkdownBlockStyles(theme); + const textStyles = getMarkdownTextStyles(theme); + const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message; + const isPendingOrFailedPost = isPending || isFailed; + + // we should check for the user roles and permissions + if (!isPendingOrFailedPost) { + if (isFlagged) { + actions.push({ + text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}), + onPress: this.unflagPost + }); + } else { + actions.push({ + text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}), + onPress: this.flagPost + }); + } + + if (canEdit) { + actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit}); + } + + if (canDelete && !hasBeenDeleted) { + actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete}); + } + } + + let messageComponent; + if (hasBeenDeleted) { + messageComponent = ( + + ); + } else if (message.length) { + messageComponent = ( + + + + + + ); + } + + return ( + + {renderReplyBar()} + + + + {messageComponent} + {this.renderSlackAttachments(messageStyle, blockStyles, textStyles)} + {this.renderFileAttachments()} + + + {isFailed && + + + + } + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + message: { + color: theme.centerChannelColor, + fontSize: 15 + }, + messageContainerWithReplyBar: { + flexDirection: 'row', + flex: 1 + }, + pendingPost: { + opacity: 0.5 + }, + systemMessage: { + opacity: 0.6 + } + }); +}); + +export default injectIntl(PostBody); diff --git a/app/components/post_header/index.js b/app/components/post_header/index.js new file mode 100644 index 000000000..4efa2e94a --- /dev/null +++ b/app/components/post_header/index.js @@ -0,0 +1,41 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; + +import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; +import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; +import {getUser} from 'mattermost-redux/selectors/entities/users'; +import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils'; +import {displayUsername} from 'mattermost-redux/utils/user_utils'; + +import {getTheme} from 'app/selectors/preferences'; + +import PostHeader from './post_header'; + +function makeMapStateToProps() { + const getCommentCountForPost = makeGetCommentCountForPost(); + return function mapStateToProps(state, ownProps) { + const {config} = state.entities.general; + const post = getPost(state, ownProps.postId); + const commentedOnUser = getUser(state, ownProps.commentedOnUserId); + const user = getUser(state, post.user_id); + const teammateNameDisplay = getTeammateNameDisplaySetting(state); + + return { + ...ownProps, + commentedOnDisplayName: displayUsername(commentedOnUser, teammateNameDisplay), + commentCount: getCommentCountForPost(state, {post}), + createAt: post.create_at, + displayName: displayUsername(user, teammateNameDisplay), + enablePostUsernameOverride: config.EnablePostUsernameOverride === 'true', + fromWebHook: post.props && post.props.from_webhook === 'true', + isPendingOrFailedPost: isPostPendingOrFailed(post), + isSystemMessage: isSystemMessage(post), + overrideUsername: post.props && post.props.override_username, + theme: getTheme(state) + }; + }; +} + +export default connect(makeMapStateToProps)(PostHeader); diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js new file mode 100644 index 000000000..97cc02ce2 --- /dev/null +++ b/app/components/post_header/post_header.js @@ -0,0 +1,239 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; + +import FormattedText from 'app/components/formatted_text'; +import FormattedTime from 'app/components/formatted_time'; +import ReplyIcon from 'app/components/reply_icon'; +import {emptyFunction} from 'app/utils/general'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +const BOT_NAME = 'BOT'; + +export default class PostHeader extends PureComponent { + static propTypes = { + commentCount: PropTypes.number, + commentedOnDisplayName: PropTypes.string, + createAt: PropTypes.number.isRequired, + displayName: PropTypes.string.isRequired, + enablePostUsernameOverride: PropTypes.bool, + fromWebHook: PropTypes.bool, + isPendingOrFailedPost: PropTypes.bool, + isSystemMessage: PropTypes.bool, + onPress: PropTypes.func, + onViewUserProfile: PropTypes.func, + overrideUsername: PropTypes.string, + renderReplies: PropTypes.bool, + theme: PropTypes.object.isRequired + }; + + static defaultProps = { + commentCount: 0, + onPress: emptyFunction, + onViewUserProfile: emptyFunction + }; + + getDisplayName = (style) => { + const { + enablePostUsernameOverride, + fromWebHook, + isSystemMessage, + onViewUserProfile, + overrideUsername + } = this.props; + + if (isSystemMessage) { + return ( + + ); + } else if (fromWebHook) { + let name = this.props.displayName; + if (overrideUsername && enablePostUsernameOverride) { + name = overrideUsername; + } + + return ( + + + {name} + + + {BOT_NAME} + + + ); + } else if (this.props.displayName) { + return ( + + + {this.props.displayName} + + + ); + } + + return ( + + ); + }; + + renderCommentedOnMessage = (style) => { + if (!this.props.renderReplies || !this.props.commentedOnDisplayName) { + return null; + } + + const displayName = this.props.commentedOnDisplayName; + + let name; + if (displayName) { + name = displayName; + } else { + name = ( + + ); + } + + let apostrophe; + if (displayName && displayName.slice(-1) === 's') { + apostrophe = '\''; + } else { + apostrophe = '\'s'; + } + + return ( + + ); + }; + + render() { + const { + commentedOnDisplayName, + commentCount, + createAt, + isPendingOrFailedPost, + onPress, + renderReplies, + theme + } = this.props; + const style = getStyleSheet(theme); + + return ( + + + + {this.getDisplayName(style)} + + + + + + + {(!commentedOnDisplayName && commentCount > 0 && renderReplies) && + + + {commentCount} + + } + + {commentedOnDisplayName !== '' && + + {this.renderCommentedOnMessage(style)} + + } + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + commentedOn: { + color: changeOpacity(theme.centerChannelColor, 0.65), + marginBottom: 3, + lineHeight: 21 + }, + postInfoContainer: { + alignItems: 'center', + flexDirection: 'row', + marginTop: 10 + }, + pendingPost: { + opacity: 0.5 + }, + timeContainer: { + justifyContent: 'center' + }, + time: { + color: theme.centerChannelColor, + fontSize: 13, + marginLeft: 5, + marginBottom: 1, + opacity: 0.5 + }, + replyIconContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center' + }, + replyText: { + fontSize: 15, + marginLeft: 3, + color: theme.linkColor + }, + botContainer: { + flexDirection: 'row' + }, + bot: { + alignSelf: 'center', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.15), + borderRadius: 2, + color: theme.centerChannelColor, + fontSize: 10, + fontWeight: '600', + marginRight: 5, + paddingVertical: 2, + paddingHorizontal: 4 + }, + displayName: { + color: theme.centerChannelColor, + fontSize: 15, + fontWeight: '600', + marginRight: 5, + marginBottom: 3 + } + }); +}); diff --git a/app/components/post_profile_picture/index.js b/app/components/post_profile_picture/index.js new file mode 100644 index 000000000..c92dc613e --- /dev/null +++ b/app/components/post_profile_picture/index.js @@ -0,0 +1,30 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; + +import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {getUser} from 'mattermost-redux/selectors/entities/users'; +import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; + +import {getTheme} from 'app/selectors/preferences'; + +import PostProfilePicture from './post_profile_picture'; + +function mapStateToProps(state, ownProps) { + const {config} = state.entities.general; + const post = getPost(state, ownProps.postId); + const user = getUser(state, post.user_id); + + return { + ...ownProps, + enablePostIconOverride: config.EnablePostIconOverride === 'true', + fromWebHook: post.props && post.props.from_webhook === 'true', + isSystemMessage: isSystemMessage(post), + overrideIconUrl: post.props && post.props.override_icon_url, + user, + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(PostProfilePicture); diff --git a/app/components/post_profile_picture/post_profile_picture.js b/app/components/post_profile_picture/post_profile_picture.js new file mode 100644 index 000000000..73ba0b672 --- /dev/null +++ b/app/components/post_profile_picture/post_profile_picture.js @@ -0,0 +1,84 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Image, TouchableOpacity, View} from 'react-native'; + +import MattermostIcon from 'app/components/mattermost_icon'; +import ProfilePicture from 'app/components/profile_picture'; +import {emptyFunction} from 'app/utils/general'; +import webhookIcon from 'assets/images/icons/webhook.jpg'; + +const PROFILE_PICTURE_SIZE = 32; + +function PostProfilePicture(props) { + const { + enablePostIconOverride, + fromWebHook, + isSystemMessage, + onViewUserProfile, + overrideIconUrl, + theme, + user + } = props; + if (isSystemMessage) { + return ( + + + + ); + } else if (fromWebHook) { + if (enablePostIconOverride) { + const icon = overrideIconUrl ? {uri: overrideIconUrl} : webhookIcon; + return ( + + + + ); + } + + return ( + + ); + } + + return ( + + + + ); +} + +PostProfilePicture.propTypes = { + enablePostIconOverride: PropTypes.bool, + fromWebHook: PropTypes.bool, + isSystemMessage: PropTypes.bool, + overrideIconUrl: PropTypes.string, + onViewUserProfile: PropTypes.func, + theme: PropTypes.object, + user: PropTypes.object +}; + +PostProfilePicture.defaultProps = { + onViewUserProfile: emptyFunction +}; + +export default PostProfilePicture; diff --git a/app/components/root/index.js b/app/components/root/index.js index dc59e3f34..65967dab6 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -23,7 +23,7 @@ function mapStateToProps(state, ownProps) { } Client.setLocale(locale); - Client4.setLocale(locale); + Client4.setAcceptLanguage(locale); return { ...ownProps, diff --git a/app/components/slack_attachments/index.js b/app/components/slack_attachments/index.js index 2c82cd629..6797bca40 100644 --- a/app/components/slack_attachments/index.js +++ b/app/components/slack_attachments/index.js @@ -15,11 +15,12 @@ export default class SlackAttachments extends PureComponent { baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, textStyles: PropTypes.object, + navigator: PropTypes.object.isRequired, theme: PropTypes.object }; render() { - const {attachments, baseTextStyle, blockStyles, textStyles, theme} = this.props; + const {attachments, baseTextStyle, blockStyles, navigator, textStyles, theme} = this.props; const content = []; attachments.forEach((attachment, i) => { @@ -30,6 +31,7 @@ export default class SlackAttachments extends PureComponent { blockStyles={blockStyles} key={'att_' + i} textStyles={textStyles} + navigator={navigator} theme={theme} /> ); diff --git a/app/components/slack_attachments/slack_attachment.js b/app/components/slack_attachments/slack_attachment.js index 117fcf805..509fd3179 100644 --- a/app/components/slack_attachments/slack_attachment.js +++ b/app/components/slack_attachments/slack_attachment.js @@ -21,6 +21,7 @@ export default class SlackAttachment extends PureComponent { attachment: PropTypes.object.isRequired, baseTextStyle: CustomPropTypes.Style, blockStyles: PropTypes.object, + navigator: PropTypes.object.isRequired, textStyles: PropTypes.object, theme: PropTypes.object }; @@ -61,6 +62,7 @@ export default class SlackAttachment extends PureComponent { attachment, baseTextStyle, blockStyles, + navigator, textStyles } = this.props; const fields = attachment.fields; @@ -115,6 +117,7 @@ export default class SlackAttachment extends PureComponent { textStyles={textStyles} blockStyles={blockStyles} value={(field.value || '')} + navigator={navigator} /> @@ -167,6 +170,7 @@ export default class SlackAttachment extends PureComponent { baseTextStyle, blockStyles, textStyles, + navigator, theme } = this.props; @@ -181,6 +185,7 @@ export default class SlackAttachment extends PureComponent { textStyles={textStyles} blockStyles={blockStyles} value={attachment.pretext} + navigator={navigator} /> ); @@ -288,6 +293,7 @@ export default class SlackAttachment extends PureComponent { textStyles={textStyles} blockStyles={blockStyles} value={this.state.text} + navigator={navigator} /> {moreLess} diff --git a/app/screens/image_preview/index.js b/app/screens/image_preview/index.js index b8cbc07b6..26fc9bc55 100644 --- a/app/screens/image_preview/index.js +++ b/app/screens/image_preview/index.js @@ -19,7 +19,7 @@ function makeMapStateToProps() { return { ...ownProps, fetchCache: state.views.fetchCache, - files: getFilesForPost(state, ownProps.post), + files: getFilesForPost(state, ownProps.postId), theme: getTheme(state), statusBarHeight: Platform.OS === 'ios' ? state.views.root.statusBarHeight : STATUSBAR_HEIGHT }; diff --git a/app/utils/general.js b/app/utils/general.js index f9c0374a3..1b460e76b 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -10,3 +10,7 @@ export function alertErrorWithFallback(intl, error, fallback, values) { } Alert.alert('', msg); } + +export function emptyFunction() { + return; +} diff --git a/yarn.lock b/yarn.lock index 3f7a44f3f..e2b5d6cc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3645,7 +3645,7 @@ makeerror@1.0.x: mattermost-redux@mattermost/mattermost-redux#master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/26b897a943a2095bdc793f28c10415441859ad6f" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e3b029e3df3d894485a27a9ca3cc7cbe7711ddb9" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1"