Commit 65315c03 authored by George Goldberg's avatar George Goldberg Committed by GitHub

PLT-7670: Make webapp robust to posts whose root post has been deleted. (#26)

* Make webapp robust to posts whose root post has been deleted.

* Localize the root post deleted message.

* Fix review comments.

* More review fixes.

* Fix redundant code.
parent b07aafc7
......@@ -157,7 +157,8 @@ export function emitCloseRightHandSide() {
dispatch({
type: ActionTypes.SELECT_POST,
postId: ''
postId: '',
channelId: ''
});
}
......@@ -167,6 +168,7 @@ export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: Utils.getRootId(post),
channelId: post.channel_id,
from_search: SearchStore.getSearchTerm(),
from_flagged_posts: SearchStore.getIsFlaggedPosts(),
from_pinned_posts: SearchStore.getIsPinnedPosts()
......@@ -522,7 +524,8 @@ export function toggleSideBarAction(visible) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
postId: null,
channelId: null
});
}
}
......@@ -535,7 +538,8 @@ export function toggleSideBarRightMenuAction() {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
postId: null,
channelId: null
});
document.querySelector('.app__body .inner-wrap').classList.remove('move--right', 'move--left', 'move--left-small');
......
......@@ -223,7 +223,8 @@ export function deletePost(channelId, post, success) {
if (post.id === getState().views.rhs.selectedPostId) {
dispatch({
type: ActionTypes.SELECT_POST,
postId: ''
postId: '',
channelId: ''
});
}
......
......@@ -144,7 +144,8 @@ export default class Navbar extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
postId: null,
channelId: null
});
if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') {
......
......@@ -108,7 +108,8 @@ export default class Post extends React.PureComponent {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: Utils.getRootId(this.props.post)
postId: Utils.getRootId(this.props.post),
channelId: this.props.post.channel_id
});
AppDispatcher.handleServerAction({
......
......@@ -51,7 +51,8 @@ export default class RhsHeaderPost extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
postId: null,
channelId: null
});
} else if (this.props.fromFlaggedPosts) {
getFlaggedPosts();
......
......@@ -133,17 +133,23 @@ export default class RhsRootPost extends React.Component {
}
renderTimeTag(post, timeOptions) {
return Utils.isMobile() ?
this.timeTag(post, timeOptions) :
(
<Link
to={`/${this.state.currentTeamDisplayName}/pl/${post.id}`}
target='_blank'
className='post__permalink'
>
{this.timeTag(post, timeOptions)}
</Link>
);
if (post.type === Constants.PostTypes.FAKE_PARENT_DELETED) {
return null;
}
if (Utils.isMobile()) {
return this.timeTag(post, timeOptions);
}
return (
<Link
to={`/${this.state.currentTeamDisplayName}/pl/${post.id}`}
target='_blank'
className='post__permalink'
>
{this.timeTag(post, timeOptions)}
</Link>
);
}
toggleEmojiPicker = () => {
......@@ -257,18 +263,23 @@ export default class RhsRootPost extends React.Component {
);
}
let userProfile = (
<UserProfile
user={user}
status={this.props.status}
isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/>
);
let userProfile;
let botIndicator;
if (post.props && post.props.from_webhook) {
if (isSystemMessage) {
userProfile = (
<UserProfile
user={{}}
overwriteName={
<FormattedMessage
id='post_info.system'
defaultMessage='System'
/>
}
overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
disablePopover={true}
/>
);
} else if (post.props && post.props.from_webhook) {
if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
userProfile = (
<UserProfile
......@@ -287,18 +298,14 @@ export default class RhsRootPost extends React.Component {
}
botIndicator = <div className='col col__name bot-indicator'>{'BOT'}</div>;
} else if (isSystemMessage) {
} else {
userProfile = (
<UserProfile
user={{}}
overwriteName={
<FormattedMessage
id='post_info.system'
defaultMessage='System'
/>
}
overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
disablePopover={true}
user={user}
status={this.props.status}
isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/>
);
}
......@@ -308,20 +315,15 @@ export default class RhsRootPost extends React.Component {
status = null;
}
let profilePic = (
<ProfilePicture
src={PostUtils.getProfilePicSrcForPost(post, user)}
status={status}
width='36'
height='36'
user={this.props.user}
isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/>
);
if (post.props && post.props.from_webhook) {
let profilePic;
if (isSystemMessage) {
profilePic = (
<span
className='icon'
dangerouslySetInnerHTML={{__html: mattermostLogo}}
/>
);
} else if (post.props && post.props.from_webhook) {
profilePic = (
<ProfilePicture
src={PostUtils.getProfilePicSrcForPost(post, user)}
......@@ -329,13 +331,17 @@ export default class RhsRootPost extends React.Component {
height='36'
/>
);
}
if (isSystemMessage) {
} else {
profilePic = (
<span
className='icon'
dangerouslySetInnerHTML={{__html: mattermostLogo}}
<ProfilePicture
src={PostUtils.getProfilePicSrcForPost(post, user)}
status={status}
width='36'
height='36'
user={this.props.user}
isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/>
);
}
......@@ -396,6 +402,30 @@ export default class RhsRootPost extends React.Component {
/>
);
let dotMenuContainer;
if (this.props.post.type !== Constants.PostTypes.FAKE_PARENT_DELETED) {
dotMenuContainer = (
<div
ref='dotMenu'
className='col col__reply'
>
{dotMenu}
{react}
</div>
);
}
let postFlagIcon;
if (this.props.post.type !== Constants.PostTypes.FAKE_PARENT_DELETED) {
postFlagIcon = (
<PostFlagIcon
idPrefix={'rhsRootPostFlag'}
postId={post.id}
isFlagged={this.props.isFlagged}
/>
);
}
return (
<div
id='thread--root'
......@@ -411,19 +441,9 @@ export default class RhsRootPost extends React.Component {
<div className='col'>
{this.renderTimeTag(post, timeOptions)}
{pinnedBadge}
<PostFlagIcon
idPrefix={'rhsRootPostFlag'}
postId={post.id}
isFlagged={this.props.isFlagged}
/>
</div>
<div
ref='dotMenu'
className='col col__reply'
>
{dotMenu}
{react}
{postFlagIcon}
</div>
{dotMenuContainer}
</div>
<div className='post__body'>
<div className={postClass}>
......
......@@ -3,16 +3,18 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
import {makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
import {removePost} from 'mattermost-redux/actions/posts';
import {getSelectedPost} from 'selectors/rhs.jsx';
import RhsThread from './rhs_thread.jsx';
function makeMapStateToProps() {
const getPostsForThread = makeGetPostsForThread();
return function mapStateToProps(state, ownProps) {
const selected = getPost(state, state.views.rhs.selectedPostId);
const selected = getSelectedPost(state);
let posts = [];
if (selected) {
posts = getPostsForThread(state, {rootId: selected.id, channelId: selected.channel_id});
......
......@@ -351,7 +351,11 @@ export default class RhsThread extends React.Component {
rootStatus = this.state.statuses[selected.user_id] || 'offline';
}
const rootPostDay = Utils.getDateForUnixTicks(selected.create_at);
let createAt = selected.create_at;
if (!createAt) {
createAt = this.props.posts[0].create_at;
}
const rootPostDay = Utils.getDateForUnixTicks(createAt);
let previousPostDay = rootPostDay;
const commentsLists = [];
......@@ -405,6 +409,20 @@ export default class RhsThread extends React.Component {
);
}
let createComment;
if (selected.type !== Constants.PostTypes.FAKE_PARENT_DELETED) {
createComment = (
<div className='post-create__container'>
<CreateComment
channelId={selected.channel_id}
rootId={selected.id}
latestPostId={postsLength > 0 ? postsArray[postsLength - 1].id : selected.id}
getSidebarBody={this.getSidebarBody}
/>
</div>
);
}
return (
<div
className='sidebar-right__body'
......@@ -457,14 +475,7 @@ export default class RhsThread extends React.Component {
>
{commentsLists}
</div>
<div className='post-create__container'>
<CreateComment
channelId={selected.channel_id}
rootId={selected.id}
latestPostId={postsLength > 0 ? postsArray[postsLength - 1].id : selected.id}
getSidebarBody={this.getSidebarBody}
/>
</div>
{createComment}
</div>
</Scrollbars>
</div>
......
......@@ -108,7 +108,8 @@ export default class SearchBar extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null
postId: null,
channelId: null
});
}
......
......@@ -2205,6 +2205,7 @@
"rhs_root.permalink": "Permalink",
"rhs_root.pin": "Pin to channel",
"rhs_root.unpin": "Un-pin from channel",
"rhs_thread.rootPostDeletedMessage.body": "Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.",
"search_bar.search": "Search",
"search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>",
"search_header.results": "Search Results",
......
......@@ -19,6 +19,15 @@ function selectedPostId(state = '', action) {
}
}
function selectedPostChannelId(state = '', action) {
switch (action.type) {
case ActionTypes.SELECT_POST:
return action.channelId;
default:
return state;
}
}
function fromSearch(state = '', action) {
switch (action.type) {
case ActionTypes.SELECT_POST:
......@@ -57,6 +66,7 @@ function fromPinnedPosts(state = false, action) {
export default combineReducers({
selectedPostId,
selectedPostChannelId,
fromSearch,
fromFlaggedPosts,
fromPinnedPosts
......
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {createSelector} from 'reselect';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {localizeMessage} from 'utils/utils.jsx';
import {PostTypes} from 'utils/constants.jsx';
export function getSelectedPostId(state) {
return state.views.rhs.selectedPostId;
}
function getSelectedPostChannelId(state) {
return state.views.rhs.selectedPostChannelId;
}
function getRealSelectedPost(state) {
return state.entities.posts.posts[state.views.rhs.selectedPostId];
}
export const getSelectedPost = createSelector(
getSelectedPostId,
getRealSelectedPost,
getSelectedPostChannelId,
getCurrentUserId,
(selectedPostId, selectedPost, selectedPostChannelId, currentUserId) => {
if (selectedPost) {
return selectedPost;
}
// If there is no root post found, assume it has been deleted by data retention policy, and create a fake one.
return {
id: selectedPostId,
exists: false,
type: PostTypes.FAKE_PARENT_DELETED,
message: localizeMessage('rhs_thread.rootPostDeletedMessage.body', 'Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.'),
channel_id: selectedPostChannelId,
user_id: currentUserId
};
}
);
......@@ -281,6 +281,7 @@ export const PostTypes = {
DISPLAYNAME_CHANGE: 'system_displayname_change',
PURPOSE_CHANGE: 'system_purpose_change',
CHANNEL_DELETED: 'system_channel_deleted',
FAKE_PARENT_DELETED: 'system_fake_parent_deleted',
EPHEMERAL: 'system_ephemeral',
REMOVE_LINK_PREVIEW: 'remove_link_preview'
};
......
......@@ -53,6 +53,10 @@ export function getProfilePicSrcForPost(post, user) {
}
export function canDeletePost(post) {
if (post.type === Constants.PostTypes.FAKE_PARENT_DELETED) {
return false;
}
const isOwner = isPostOwner(post);
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam() || isSystemAdmin;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment