Unverified Commit 2cc413f8 authored by picklesrus's avatar picklesrus Committed by GitHub

Merge pull request #5587 from picklesrus/studio-comments-off

Studio comments off
parents 588ee632 740e8c19
......@@ -142,6 +142,8 @@ module.exports.selectIsSocial = state => get(state, ['session', 'session', 'perm
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
module.exports.selectProjectCommentsGloballyEnabled = state =>
get(state, ['session', 'session', 'flags', 'project_comments_enabled'], false);
module.exports.selectStudioCommentsGloballyEnabled = state =>
get(state, ['session', 'session', 'flags', 'gallery_comments_enabled'], false);
module.exports.selectMuteStatus = state => get(state, ['session', 'session', 'permissions', 'mute_status'],
{muteExpiresAt: 0, offenses: [], showWarning: false});
module.exports.selectIsMuted = state => (module.exports.selectMuteStatus(state).muteExpiresAt || 0) * 1000 > Date.now();
......
const {selectUserId, selectIsAdmin, selectIsSocial,
selectIsLoggedIn, selectUsername, selectIsMuted} = require('./session');
selectIsLoggedIn, selectUsername, selectIsMuted,
selectHasFetchedSession, selectStudioCommentsGloballyEnabled} = require('./session');
// Fine-grain selector helpers - not exported, use the higher level selectors below
const isCreator = state => selectUserId(state) === state.studio.owner;
......@@ -73,7 +74,9 @@ const selectShowProjectMuteError = state => selectIsMuted(state) &&
isCurator(state) ||
(selectIsSocial(state) && state.studio.openToAll));
const selectShowCuratorMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state));
const selectShowCommentsGloballyOffError = state =>
selectHasFetchedSession(state) && !selectStudioCommentsGloballyEnabled(state);
const selectShowCommentsList = state => selectHasFetchedSession(state) && selectStudioCommentsGloballyEnabled(state);
export {
selectCanEditInfo,
selectCanAddProjects,
......@@ -92,6 +95,8 @@ export {
selectCanRemoveManager,
selectCanPromoteCurators,
selectCanRemoveProject,
selectShowCommentsList,
selectShowCommentsGloballyOffError,
selectShowEditMuteError,
selectShowProjectMuteError,
selectShowCuratorMuteError
......
......@@ -90,6 +90,7 @@
"studio.comments.toggleOff": "Commenting off",
"studio.comments.toggleOn": "Commenting on",
"studio.comments.turnedOff": "Sorry, comment posting has been turned off for this studio.",
"studio.comments.turnedOffGlobally" : "Studio comments across Scratch are turned off, but don't worry, your comments are saved and will be back soon.",
"studio.sharedFilter": "Shared",
"studio.favoritedFilter": "Favorited",
......
......@@ -4,12 +4,12 @@ import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import Button from '../../components/forms/button.jsx';
import CommentingStatus from '../../components/commenting-status/commenting-status.jsx';
import ComposeComment from '../preview/comment/compose-comment.jsx';
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import StudioCommentsAllowed from './studio-comments-allowed.jsx';
import StudioCommentsNotAllowed from './studio-comments-not-allowed.jsx';
import {selectIsAdmin, selectHasFetchedSession, selectUsername} from '../../redux/session';
import {
selectShowCommentComposer,
......@@ -18,7 +18,9 @@ import {
selectCanDeleteCommentWithoutConfirm,
selectCanReportComment,
selectCanRestoreComment,
selectCanEditCommentsAllowed
selectCanEditCommentsAllowed,
selectShowCommentsList,
selectShowCommentsGloballyOffError
} from '../../redux/studio-permissions';
import {selectStudioCommentsAllowed} from '../../redux/studio.js';
......@@ -33,6 +35,8 @@ const StudioComments = ({
replies,
postURI,
shouldShowCommentComposer,
shouldShowCommentsList,
shouldShowCommentsGloballyOffError,
username,
canDeleteAnyComment,
canDeleteOwnComment,
......@@ -78,13 +82,13 @@ const StudioComments = ({
}, [isAdmin]);
const [replyStatusCommentId, setReplyStatusCommentId] = useState('');
const hasReplyStatus = function (comment) {
return (
comment.parent_id && comment.parent_id === replyStatusCommentId
) || (comment.id === replyStatusCommentId);
};
const handleReplyStatusChange = function (id) {
setReplyStatusCommentId(id);
};
......@@ -95,48 +99,63 @@ const StudioComments = ({
<h2><FormattedMessage id="studio.commentsHeader" /></h2>
{canEditCommentsAllowed && <StudioCommentsAllowed />}
</div>
{shouldShowCommentsGloballyOffError &&
<div>
{shouldShowCommentComposer ?
(commentsAllowed ?
<ComposeComment
<CommentingStatus>
<p>
<FormattedMessage id="studio.comments.turnedOffGlobally" />
</p>
</CommentingStatus>
<img
className="studio-comment-placholder-img"
src="/images/comments/comment-placeholder.png"
/>
</div>
}
{shouldShowCommentsList &&
<div>
{shouldShowCommentComposer ?
(commentsAllowed ?
<ComposeComment
postURI={postURI}
onAddComment={handleNewComment}
/> :
<StudioCommentsNotAllowed />
) : null
}
{comments.map(comment => (
<TopLevelComment
hasThreadLimit
author={comment.author}
canDelete={canDeleteAnyComment ||
(canDeleteOwnComment && comment.author.username === username)}
canDeleteWithoutConfirm={canDeleteCommentWithoutConfirm}
canReply={shouldShowCommentComposer}
canReport={canReportComment}
canRestore={canRestoreComment}
content={comment.content}
datetimeCreated={comment.datetime_created}
defaultExpanded={singleCommentId}
highlightedCommentId={singleCommentId}
id={comment.id}
key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id}
postURI={postURI}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
threadHasReplyStatus={hasReplyStatus(comment)}
totalReplyCount={comment.reply_count}
visibility={comment.visibility}
onAddComment={handleNewComment}
/> :
<StudioCommentsNotAllowed />
) : null
}
{comments.map(comment => (
<TopLevelComment
hasThreadLimit
author={comment.author}
canDelete={canDeleteAnyComment || (canDeleteOwnComment && comment.author.username === username)}
canDeleteWithoutConfirm={canDeleteCommentWithoutConfirm}
canReply={shouldShowCommentComposer}
canReport={canReportComment}
canRestore={canRestoreComment}
content={comment.content}
datetimeCreated={comment.datetime_created}
defaultExpanded={singleCommentId}
highlightedCommentId={singleCommentId}
id={comment.id}
key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id}
postURI={postURI}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
threadHasReplyStatus={hasReplyStatus(comment)}
totalReplyCount={comment.reply_count}
visibility={comment.visibility}
onAddComment={handleNewComment}
onDelete={handleDeleteComment}
onRestore={handleRestoreComment}
// eslint-disable-next-line react/jsx-no-bind
onReply={handleReplyStatusChange}
onReport={handleReportComment}
onLoadMoreReplies={handleLoadMoreReplies}
/>
))}
{!!singleCommentId &&
onDelete={handleDeleteComment}
onRestore={handleRestoreComment}
// eslint-disable-next-line react/jsx-no-bind
onReply={handleReplyStatusChange}
onReport={handleReportComment}
onLoadMoreReplies={handleLoadMoreReplies}
/>
))}
{!!singleCommentId &&
<Button
className="button load-more-button"
// eslint-disable-next-line react/jsx-no-bind
......@@ -144,16 +163,17 @@ const StudioComments = ({
>
<FormattedMessage id="general.seeAllComments" />
</Button>
}
{moreCommentsToLoad &&
<Button
className="button load-more-button"
onClick={handleLoadMoreComments}
>
<FormattedMessage id="general.loadMore" />
</Button>
}
</div>
}
{moreCommentsToLoad &&
<Button
className="button load-more-button"
onClick={handleLoadMoreComments}
>
<FormattedMessage id="general.loadMore" />
</Button>
}
</div>
}
</div>
);
};
......@@ -168,6 +188,8 @@ StudioComments.propTypes = {
moreCommentsToLoad: PropTypes.bool,
replies: PropTypes.shape({}),
shouldShowCommentComposer: PropTypes.bool,
shouldShowCommentsGloballyOffError: PropTypes.bool,
shouldShowCommentsList: PropTypes.bool,
username: PropTypes.string,
canDeleteAnyComment: PropTypes.bool,
canDeleteOwnComment: PropTypes.bool,
......@@ -198,6 +220,8 @@ export default connect(
username: selectUsername(state),
commentsAllowed: selectStudioCommentsAllowed(state),
shouldShowCommentComposer: selectShowCommentComposer(state),
shouldShowCommentsGloballyOffError: selectShowCommentsGloballyOffError(state),
shouldShowCommentsList: selectShowCommentsList(state),
canDeleteAnyComment: selectCanDeleteAnyComment(state),
canDeleteOwnComment: selectCanDeleteOwnComment(state),
canDeleteCommentWithoutConfirm: selectCanDeleteCommentWithoutConfirm(state),
......
......@@ -472,6 +472,10 @@ $radius: 8px;
}
}
.studio-comment-placholder-img {
width: 100%;
}
.studio-managers-header {
justify-content: flex-start;
}
......
......@@ -69,4 +69,95 @@ describe('Studio comments', () => {
);
expect(resetComments).not.toHaveBeenCalled();
});
test('Comments do not show when shouldShowCommentsList is false', () => {
const component = mountWithIntl(
<StudioComments
hasFetchedSession
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsList={false}
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('TopLevelComment').exists()).toBe(false);
});
test('Comments show when shouldShowCommentsList is true', () => {
const component = mountWithIntl(
<StudioComments
hasFetchedSession
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsList
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('TopLevelComment').exists()).toBe(true);
});
test('Single comment load more shows when shouldShowCommentsList is true', () => {
// Make the component think this is a single view.
global.window.location.hash = '#comments-6';
const component = mountWithIntl(
<StudioComments
hasFetchedSession
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsList
singleCommentId
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('TopLevelComment').exists()).toBe(true);
expect(component.find('Button').exists()).toBe(true);
expect(component.find('button.load-more-button').exists()).toBe(true);
global.window.location.hash = '';
});
test('Single comment does not show when shouldShowCommentsList is false', () => {
// Make the component think this is a single view.
global.window.location.hash = '#comments-6';
const component = mountWithIntl(
<StudioComments
hasFetchedSession
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsList={false}
singleCommentId
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('TopLevelComment').exists()).toBe(false);
expect(component.find('Button').exists()).toBe(false);
expect(component.find('button.load-more-button').exists()).toBe(false);
global.window.location.hash = '';
});
test('Comment status error shows when shoudlShowCommentsGloballyOffError is true', () => {
const component = mountWithIntl(
<StudioComments
hasFetchedSession={false}
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsGloballyOffError
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('CommentingStatus').exists()).toBe(true);
});
test('Comment status error does not show when shoudlShowCommentsGloballyOffError is false', () => {
const component = mountWithIntl(
<StudioComments
hasFetchedSession={false}
isAdmin={false}
comments={[{id: 123, author: {}}]}
shouldShowCommentsGloballyOffError={false}
/>
);
expect(component.find('div.studio-compose-container').exists()).toBe(true);
expect(component.find('CommentingStatus').exists()).toBe(false);
});
});
......@@ -16,13 +16,16 @@ import {
selectCanRemoveManager,
selectCanPromoteCurators,
selectCanRemoveProject,
selectShowCommentsList,
selectShowCommentsGloballyOffError,
selectShowProjectMuteError,
selectShowCuratorMuteError,
selectShowEditMuteError
} from '../../../src/redux/studio-permissions';
import {getInitialState as getInitialStudioState} from '../../../src/redux/studio';
import {getInitialState as getInitialSessionState, selectUserId, selectUsername} from '../../../src/redux/session';
import {getInitialState as getInitialSessionState,
selectUserId, selectUsername, Status} from '../../../src/redux/session';
import {sessions, studios} from '../../helpers/state-fixtures.json';
let state;
......@@ -503,4 +506,81 @@ describe('studio mute errors', () => {
expect(selectShowEditMuteError(state)).toBe(expected);
});
});
describe('show comments list selector', () => {
test('show comments is true', () => {
const thisSession = {
status: Status.FETCHED,
session: {
flags: {
gallery_comments_enabled: true
}
}
};
state.session = thisSession;
expect(selectShowCommentsList(state)).toBe(true);
});
test('show comments is false because feature flag is false', () => {
const thisSession = {
status: Status.FETCHED,
session: {
flags: {
gallery_comments_enabled: false
}
}
};
state.session = thisSession;
expect(selectShowCommentsList(state)).toBe(false);
});
test('show comments is false because session not fetched', () => {
const thisSession = {
status: Status.NOT_FETCHED,
session: {
flags: {
gallery_comments_enabled: true
}
}
};
state.session = thisSession;
expect(selectShowCommentsList(state)).toBe(false);
});
});
describe('show comments globally off error', () => {
test('show comments off error because feature flag is false', () => {
const thisSession = {
status: Status.FETCHED,
session: {
flags: {
gallery_comments_enabled: false
}
}
};
state.session = thisSession;
expect(selectShowCommentsGloballyOffError(state)).toBe(true);
});
test('Do not show comments off error because feature flag is on ', () => {
const thisSession = {
status: Status.FETCHED,
session: {
flags: {
gallery_comments_enabled: true
}
}
};
state.session = thisSession;
expect(selectShowCommentsGloballyOffError(state)).toBe(false);
});
test('Do not show comments off error because session not fetched ', () => {
const thisSession = {
status: Status.NOT_FETCHED,
session: {
flags: {
gallery_comments_enabled: false
}
}
};
state.session = thisSession;
expect(selectShowCommentsGloballyOffError(state)).toBe(false);
});
});
});
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