From 122160726dc317adc9339f695b249a33c4db239a Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 20 Nov 2018 11:59:50 -0500 Subject: [PATCH 1/4] Show details about why a project is not public Follow up the project info request with a request to the visibility endpoint to find out if the project is trashed or censored. The project just not being published is handled by the existing code. This PR generalizes the ShareBanner to a more generic "Banner" that is then filled with the relevant content. --- src/_colors.scss | 3 ++ src/redux/preview.js | 36 ++++++++++++++++++-- src/views/preview/banner.jsx | 34 +++++++++++++++++++ src/views/preview/banner.scss | 36 ++++++++++++++++++++ src/views/preview/l10n.json | 3 +- src/views/preview/presentation.jsx | 51 +++++++++++++++++++++++++---- src/views/preview/preview.jsx | 18 +++++++--- src/views/preview/share-banner.jsx | 29 ---------------- src/views/preview/share-banner.scss | 35 -------------------- 9 files changed, 167 insertions(+), 78 deletions(-) create mode 100644 src/views/preview/banner.jsx create mode 100644 src/views/preview/banner.scss delete mode 100644 src/views/preview/share-banner.jsx delete mode 100644 src/views/preview/share-banner.scss diff --git a/src/_colors.scss b/src/_colors.scss index 33c5560c3..08e4cfcae 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -8,6 +8,9 @@ $ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary $ui-orange-10percent: hsla(35, 90, 55, .1); $ui-orange-25percent: hsla(35, 90, 55, .25); +$ui-red: hsla(20, 100%, 55%, 1); /* #FF661A */ +$ui-red-25percent: hsla(20, 100%, 55%, .25); + $ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA $ui-gray: hsla(0, 0, 95, 1); //#F2F2F2 $ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3 diff --git a/src/redux/preview.js b/src/redux/preview.js index 9216209fb..e4f234bfd 100644 --- a/src/redux/preview.js +++ b/src/redux/preview.js @@ -25,6 +25,7 @@ module.exports.getInitialState = () => ({ report: module.exports.Status.NOT_FETCHED, projectStudios: module.exports.Status.NOT_FETCHED, curatedStudios: module.exports.Status.NOT_FETCHED, + visibility: module.exports.Status.NOT_FETCHED, studioRequests: {} }, projectInfo: {}, @@ -39,7 +40,8 @@ module.exports.getInitialState = () => ({ curatedStudios: [], currentStudioIds: [], moreCommentsToLoad: false, - projectNotAvailable: false + projectNotAvailable: false, + visibilityInfo: {} }); module.exports.previewReducer = (state, action) => { @@ -169,6 +171,10 @@ module.exports.previewReducer = (state, action) => { return Object.assign({}, state, { moreCommentsToLoad: action.moreCommentsToLoad }); + case 'SET_VISIBILITY_INFO': + return Object.assign({}, state, { + visibilityInfo: action.visibilityInfo + }); case 'ERROR': log.error(action.error); return state; @@ -321,7 +327,12 @@ module.exports.resetComments = () => ({ type: 'RESET_COMMENTS' }); -module.exports.getProjectInfo = (id, token) => (dispatch => { +module.exports.setVisibilityInfo = visibilityInfo => ({ + type: 'SET_VISIBILITY_INFO', + visibilityInfo: visibilityInfo +}); + +module.exports.getProjectInfo = (id, username, token) => (dispatch => { const opts = { uri: `/projects/${id}` }; @@ -343,6 +354,27 @@ module.exports.getProjectInfo = (id, token) => (dispatch => { } dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHED)); dispatch(module.exports.setProjectInfo(body)); + + // If the project is not public, make a follow-up request for why + if (username && !body.public) { + dispatch(module.exports.getVisibilityInfo(id, username, token)); + } + }); +}); + +module.exports.getVisibilityInfo = (id, username, token) => (dispatch => { + dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHING)); + api({ + uri: `/users/${username}/projects/${id}/visibility`, + authentication: token + }, (err, body, response) => { + if (err || !body || response.statusCode !== 200) { + dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.ERROR)); + dispatch(module.exports.setError('No visibility info available')); + return; + } + dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHED)); + dispatch(module.exports.setVisibilityInfo(body)); }); }); diff --git a/src/views/preview/banner.jsx b/src/views/preview/banner.jsx new file mode 100644 index 000000000..c3ab9bcc6 --- /dev/null +++ b/src/views/preview/banner.jsx @@ -0,0 +1,34 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const classNames = require('classnames'); +const FlexRow = require('../../components/flex-row/flex-row.jsx'); +const Button = require('../../components/forms/button.jsx'); + +require('./banner.scss'); + +const Banner = ({className, message, actionMessage, onAction}) => ( +
+ + + {message} + + {actionMessage && onAction && ( + + )} + +
+); + +Banner.propTypes = { + actionMessage: PropTypes.node, + className: PropTypes.string, + message: PropTypes.node.isRequired, + onAction: PropTypes.func +}; + +module.exports = Banner; diff --git a/src/views/preview/banner.scss b/src/views/preview/banner.scss new file mode 100644 index 000000000..8499e3f6f --- /dev/null +++ b/src/views/preview/banner.scss @@ -0,0 +1,36 @@ +@import "../../colors"; + +$navigation-height: 50px; + +.banner-outer { + background-color: $ui-orange-25percent; + width: 100%; + overflow: hidden; + color: $ui-orange; + font-weight: bold; +} + +.banner-outer.banner-danger { + background-color: $ui-red-25percent; + color: $ui-red; +} + +.banner-inner { + min-height: 60px; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; +} + +.banner-text { + padding: 0.5rem 0; +} + +.banner-button { + background-color: $ui-orange; + font-size: .875rem; +} + +.banner-danger .banner-button { + background-color: $ui-red; +} diff --git a/src/views/preview/l10n.json b/src/views/preview/l10n.json index e237c2b44..7f0e429e1 100644 --- a/src/views/preview/l10n.json +++ b/src/views/preview/l10n.json @@ -20,5 +20,6 @@ "project.inviteToRemix": "Invite user to remix", "project.instructionsLabel": "Instructions", "project.notesAndCreditsLabel": "Notes and Credits", - "project.credit": "Thanks to {userLink} for the original project {projectLink}." + "project.credit": "Thanks to {userLink} for the original project {projectLink}.", + "project.deletedBanner": "Note: This project is in the trash folder" } diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index 30b1eb1f5..de9d8b215 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -15,7 +15,7 @@ const decorateText = require('../../lib/decorate-text.jsx'); const FlexRow = require('../../components/flex-row/flex-row.jsx'); const Button = require('../../components/forms/button.jsx'); const Avatar = require('../../components/avatar/avatar.jsx'); -const ShareBanner = require('./share-banner.jsx'); +const Banner = require('./banner.jsx'); const RemixCredit = require('./remix-credit.jsx'); const RemixList = require('./remix-list.jsx'); const Stats = require('./stats.jsx'); @@ -96,17 +96,50 @@ const PreviewPresentation = ({ replies, reportOpen, singleCommentId, - userOwnsProject + userOwnsProject, + visibilityInfo }) => { const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : ''; + // Allow embedding html in banner messages coming from the server + const embedCensorMessage = message => ( + // eslint-disable-next-line react/no-danger + + ); + + let banner; + if (visibilityInfo.censored) { + if (visibilityInfo.reshareable) { + banner = (} + className="banner-danger" + message={embedCensorMessage(visibilityInfo.censorMessage)} + onAction={onShare} + />); + } else { + banner = (); + } + } else if (visibilityInfo.deleted) { + banner = (} + />); + } else if (canShare && !isShared) { + banner = (} + message={} + onAction={onShare} + />); + } + return (
- {canShare && !isShared && ( - - )} { projectInfo && projectInfo.author && projectInfo.author.id && ( + {banner}
@@ -507,7 +540,13 @@ PreviewPresentation.propTypes = { replies: PropTypes.objectOf(PropTypes.array), reportOpen: PropTypes.bool, singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), - userOwnsProject: PropTypes.bool + userOwnsProject: PropTypes.bool, + visibilityInfo: PropTypes.shape({ + censored: PropTypes.bool, + censorMessage: PropTypes.string, + deleted: PropTypes.bool, + reshareable: PropTypes.bool + }) }; module.exports = injectIntl(PreviewPresentation); diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx index 61b673b9a..3fa9279f0 100644 --- a/src/views/preview/preview.jsx +++ b/src/views/preview/preview.jsx @@ -140,7 +140,7 @@ class Preview extends React.Component { this.props.getTopLevelComments(this.state.projectId, this.props.comments.length, this.props.isAdmin, token); } - this.props.getProjectInfo(this.state.projectId, token); + this.props.getProjectInfo(this.state.projectId, username, token); this.props.getRemixes(this.state.projectId, token); this.props.getProjectStudios(this.state.projectId, token); this.props.getCuratedStudios(username); @@ -459,6 +459,7 @@ class Preview extends React.Component { reportOpen={this.state.reportOpen} singleCommentId={this.state.singleCommentId} userOwnsProject={this.props.userOwnsProject} + visibilityInfo={this.props.visibilityInfo} onAddComment={this.handleAddComment} onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClosed={this.handleAddToStudioClose} @@ -597,7 +598,13 @@ Preview.propTypes = { classroomId: PropTypes.string }), userOwnsProject: PropTypes.bool, - userPresent: PropTypes.bool + userPresent: PropTypes.bool, + visibilityInfo: PropTypes.shape({ + censored: PropTypes.bool, + censorMessage: PropTypes.string, + deleted: PropTypes.bool, + reshareable: PropTypes.bool + }) }; Preview.defaultProps = { @@ -665,7 +672,8 @@ const mapStateToProps = state => { sessionStatus: state.session.status, // check if used user: state.session.session.user, userOwnsProject: userOwnsProject, - userPresent: userPresent + userPresent: userPresent, + visibilityInfo: state.preview.visibilityInfo }; }; @@ -707,8 +715,8 @@ const mapDispatchToProps = dispatch => ({ getParentInfo: id => { dispatch(previewActions.getParentInfo(id)); }, - getProjectInfo: (id, token) => { - dispatch(previewActions.getProjectInfo(id, token)); + getProjectInfo: (id, username, token) => { + dispatch(previewActions.getProjectInfo(id, username, token)); }, getRemixes: id => { dispatch(previewActions.getRemixes(id)); diff --git a/src/views/preview/share-banner.jsx b/src/views/preview/share-banner.jsx deleted file mode 100644 index 725cbcf19..000000000 --- a/src/views/preview/share-banner.jsx +++ /dev/null @@ -1,29 +0,0 @@ -const PropTypes = require('prop-types'); -const React = require('react'); -const FormattedMessage = require('react-intl').FormattedMessage; -const FlexRow = require('../../components/flex-row/flex-row.jsx'); -const Button = require('../../components/forms/button.jsx'); - -require('./share-banner.scss'); - -const ShareBanner = ({onShare}) => ( -
- - - - - - -
-); - -ShareBanner.propTypes = { - onShare: PropTypes.func -}; - -module.exports = ShareBanner; diff --git a/src/views/preview/share-banner.scss b/src/views/preview/share-banner.scss deleted file mode 100644 index 0524699f4..000000000 --- a/src/views/preview/share-banner.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import "../../colors"; - -$navigation-height: 50px; - -.share-banner-outer { - background-color: $ui-orange-25percent; - width: 100%; - overflow: hidden; - color: $ui-orange; -} - -.share-banner { - align-items: center; - justify-content: space-between; -} - -.share-button { - background-color: $ui-orange; - font-size: .875rem; - font-weight: normal; - - // don't show an image in share button, for now. - // &:before { - // display: inline-block; - // margin-right: .5rem; - // background-image: url("/svgs/project/share-white.svg"); - // background-repeat: no-repeat; - // background-position: center center; - // background-size: contain; - // width: 1.25rem; - // height: 1.25rem; - // vertical-align: middle; - // content: ""; - // } -} From c18ccba6464ed23bda047c1ae0781fc7a52fb258 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 20 Nov 2018 12:16:10 -0500 Subject: [PATCH 2/4] Fix scss linting --- src/views/preview/banner.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/preview/banner.scss b/src/views/preview/banner.scss index 8499e3f6f..8f753cabd 100644 --- a/src/views/preview/banner.scss +++ b/src/views/preview/banner.scss @@ -23,7 +23,7 @@ $navigation-height: 50px; } .banner-text { - padding: 0.5rem 0; + padding: .5rem 0; } .banner-button { From 0b8c13fb401029857b12649e740ab531928a1517 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 21 Nov 2018 10:31:10 -0500 Subject: [PATCH 3/4] Use the project author username in visibility request instead of viewer /ht @chrisgarrity --- src/redux/preview.js | 10 +++++----- src/views/preview/preview.jsx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/redux/preview.js b/src/redux/preview.js index e4f234bfd..fccdc0830 100644 --- a/src/redux/preview.js +++ b/src/redux/preview.js @@ -332,7 +332,7 @@ module.exports.setVisibilityInfo = visibilityInfo => ({ visibilityInfo: visibilityInfo }); -module.exports.getProjectInfo = (id, username, token) => (dispatch => { +module.exports.getProjectInfo = (id, token) => (dispatch => { const opts = { uri: `/projects/${id}` }; @@ -356,16 +356,16 @@ module.exports.getProjectInfo = (id, username, token) => (dispatch => { dispatch(module.exports.setProjectInfo(body)); // If the project is not public, make a follow-up request for why - if (username && !body.public) { - dispatch(module.exports.getVisibilityInfo(id, username, token)); + if (!body.public) { + dispatch(module.exports.getVisibilityInfo(id, body.author.username, token)); } }); }); -module.exports.getVisibilityInfo = (id, username, token) => (dispatch => { +module.exports.getVisibilityInfo = (id, ownerUsername, token) => (dispatch => { dispatch(module.exports.setFetchStatus('visibility', module.exports.Status.FETCHING)); api({ - uri: `/users/${username}/projects/${id}/visibility`, + uri: `/users/${ownerUsername}/projects/${id}/visibility`, authentication: token }, (err, body, response) => { if (err || !body || response.statusCode !== 200) { diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx index 3fa9279f0..9a1a33f1f 100644 --- a/src/views/preview/preview.jsx +++ b/src/views/preview/preview.jsx @@ -140,7 +140,7 @@ class Preview extends React.Component { this.props.getTopLevelComments(this.state.projectId, this.props.comments.length, this.props.isAdmin, token); } - this.props.getProjectInfo(this.state.projectId, username, token); + this.props.getProjectInfo(this.state.projectId, token); this.props.getRemixes(this.state.projectId, token); this.props.getProjectStudios(this.state.projectId, token); this.props.getCuratedStudios(username); @@ -715,8 +715,8 @@ const mapDispatchToProps = dispatch => ({ getParentInfo: id => { dispatch(previewActions.getParentInfo(id)); }, - getProjectInfo: (id, username, token) => { - dispatch(previewActions.getProjectInfo(id, username, token)); + getProjectInfo: (id, token) => { + dispatch(previewActions.getProjectInfo(id, token)); }, getRemixes: id => { dispatch(previewActions.getRemixes(id)); From 2ed74a2682352d872c8362fb179c31e9ed856567 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 21 Nov 2018 10:36:27 -0500 Subject: [PATCH 4/4] Show deleted banner if both deleted and censored. This is because the user can take an action to remove the trash status, but not the censored status. --- src/views/preview/presentation.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index de9d8b215..02a57b255 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -108,7 +108,12 @@ const PreviewPresentation = ({ ); let banner; - if (visibilityInfo.censored) { + if (visibilityInfo.deleted) { // If both censored and deleted, prioritize deleted banner + banner = (} + />); + } else if (visibilityInfo.censored) { if (visibilityInfo.reshareable) { banner = (} @@ -122,11 +127,6 @@ const PreviewPresentation = ({ message={embedCensorMessage(visibilityInfo.censorMessage)} />); } - } else if (visibilityInfo.deleted) { - banner = (} - />); } else if (canShare && !isShared) { banner = (}