mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 17:45:52 -05:00
Merge pull request #5576 from LLK/release/2021-06-09
This commit is contained in:
commit
7f03b4265d
18 changed files with 256 additions and 113 deletions
50
.github/workflows/main.yml
vendored
Normal file
50
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
name: Create release branch and PRs
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
- name: Set output variables
|
||||
id: vars
|
||||
run: |
|
||||
echo ::set-output name=branch::"release/$(date +%Y-%m-%d)"
|
||||
- name: Create release branch and merge develop
|
||||
run: |
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
git config --global user.name "${{ github.actor }}"
|
||||
git checkout -b ${{ steps.vars.outputs.branch }}
|
||||
git merge origin/develop
|
||||
git push --set-upstream origin ${{ steps.vars.outputs.branch }}
|
||||
- name: Develop PR
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: ${{ steps.vars.outputs.branch }}
|
||||
destination_branch: develop
|
||||
pr_title: "Develop ${{ steps.vars.outputs.branch }}"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_allow_empty: true
|
||||
- name: Master PR
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
source_branch: ${{ steps.vars.outputs.branch }}
|
||||
destination_branch: master
|
||||
pr_title: "Master ${{ steps.vars.outputs.branch }}"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_allow_empty: true
|
||||
|
56
package-lock.json
generated
56
package-lock.json
generated
|
@ -503,9 +503,9 @@
|
|||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.743",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.743.tgz",
|
||||
"integrity": "sha512-K2wXfo9iZQzNJNx67+Pld0DRF+9bYinj62gXCdgPhcu1vidwVuLPHQPPFnCdO55njWigXXpfBiT90jGUPbw8Zg==",
|
||||
"version": "1.3.749",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz",
|
||||
"integrity": "sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
|
@ -1369,9 +1369,9 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.2.tgz",
|
||||
"integrity": "sha512-sZ4GHcitIBz837RSatMDr/6GkYT8hUDffvW+bo2N5lRrBmLk37i0IGSmsxZ6QcxLu2GuymN/4gATEyL/50z3Bg==",
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.3.tgz",
|
||||
"integrity": "sha512-DBrRUL65m4SVtfq+T4Qltd8+upAzfb9K1MX0UZ0hqQ0wpBY0PSIti9XJe0ZQ/j2v/KxpwQ0Jw5NLumKVezJFQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
|
@ -1404,12 +1404,12 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-locale": {
|
||||
"version": "2.4.29",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.29.tgz",
|
||||
"integrity": "sha512-pzcnJi7CP/29aFLm8cch00r0kRWIYliyxRCUIOwdwVmCtQxbMBlxExzP/xPmyJgoAuONy42Ol1Y9Cabgy6r8XA==",
|
||||
"version": "2.4.32",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.32.tgz",
|
||||
"integrity": "sha512-jA6f4ASAO3P4mBl+1nxc+lhx6x74wP+ahUOKPK9tPYZmCn7JPTAlit0iWae79vH3wE9TblOTUZ9RwYlOPWzchw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.9.2",
|
||||
"@formatjs/ecma402-abstract": "1.9.3",
|
||||
"@formatjs/intl-getcanonicallocales": "1.7.0",
|
||||
"cldr-core": "38",
|
||||
"tslib": "^2.1.0"
|
||||
|
@ -1424,12 +1424,12 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-pluralrules": {
|
||||
"version": "4.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.24.tgz",
|
||||
"integrity": "sha512-IcCzjuKMBCd3fCcvoQJBsVrfFFcyNgyihS0wdZip9Cv8r41eOGS6oT0haQvbWoAI9ANRh/q3MtqG3Qh9QT1RGw==",
|
||||
"version": "4.0.27",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.27.tgz",
|
||||
"integrity": "sha512-Q4RAXZXuhWwMWK3Vsbf9AIRBa9B+BTiWjNkzlCq77pCRQYo555owWrGxnZUVk2203wygOIkoRYSvP7Tu2RXNFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.9.2",
|
||||
"@formatjs/ecma402-abstract": "1.9.3",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -20855,9 +20855,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-blocks": {
|
||||
"version": "0.1.0-prerelease.20210602032919",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210602032919.tgz",
|
||||
"integrity": "sha512-400Ap0AFCIaGiT7dNPVJ0nPCSTtBqId+bJuoo6tPh9WFzMOF2FsbiX1sFtkNnn1QxCfsgRN0EK5iGb0YfuSmLg==",
|
||||
"version": "0.1.0-prerelease.20210609033722",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210609033722.tgz",
|
||||
"integrity": "sha512-l6aefwO29R+I13QlDzwmjAmqp1ZoGm2V94nyOdeOMbjr41RWNeCxn0uxFFBH7Mztg7bVylJ2a5bRF4FtpcBPAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exports-loader": "0.6.3",
|
||||
|
@ -20865,9 +20865,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-gui": {
|
||||
"version": "0.1.0-prerelease.20210602035258",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210602035258.tgz",
|
||||
"integrity": "sha512-zJYMc7ACT4oQWVVQZoS5Zgsvqct8vu9p0hIiU67ySF0/WhCF38tsk66SoTYDl0Oa4xzs7kIoKIqt8Hh/+rviXg==",
|
||||
"version": "0.1.0-prerelease.20210609041525",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210609041525.tgz",
|
||||
"integrity": "sha512-sSQbdaOTeEdTjYdCV2E+7u8eJZ9Vr+8fPhlsX0sdMRI3cpiHLg6YIJwSVQ7T/NYHPyUSoAYjfL44ufF9HGQ+sA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arraybuffer-loader": "^1.0.6",
|
||||
|
@ -20918,8 +20918,8 @@
|
|||
"redux": "3.7.2",
|
||||
"redux-throttle": "0.1.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20200528195344",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210602032919",
|
||||
"scratch-l10n": "3.11.20210602031702",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210609033722",
|
||||
"scratch-l10n": "3.11.20210609031630",
|
||||
"scratch-paint": "0.2.0-prerelease.20210407203313",
|
||||
"scratch-render": "0.1.0-prerelease.20210325231800",
|
||||
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
|
||||
|
@ -21088,9 +21088,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.743",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.743.tgz",
|
||||
"integrity": "sha512-K2wXfo9iZQzNJNx67+Pld0DRF+9bYinj62gXCdgPhcu1vidwVuLPHQPPFnCdO55njWigXXpfBiT90jGUPbw8Zg==",
|
||||
"version": "1.3.750",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.750.tgz",
|
||||
"integrity": "sha512-Eqy9eHNepZxJXT+Pc5++zvEi5nQ6AGikwFYDCYwXUFBr+ynJ6pDG7MzZmwGYCIuXShLJM0n4bq+aoKDmvSGJ8A==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
|
@ -21376,9 +21376,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.11.20210602031702",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210602031702.tgz",
|
||||
"integrity": "sha512-8WCD4B/GLtAepFvJc29fGwMI0yH7ZuRwDLO2oUy8gbdCw1piJBtJVNv9nrsqIE+GjgQYn87anglf9vDb34m/ig==",
|
||||
"version": "3.11.20210609031630",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210609031630.tgz",
|
||||
"integrity": "sha512-gqBAjoWNYPm6KY5TlFIdkxnWQO3cjuwO89EQj9DeX16iS9B3n+l8E7NrO0uvZPSs2MTOjGXLkD0sWS8iyI18iA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
|
|
@ -126,8 +126,8 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20210602035258",
|
||||
"scratch-l10n": "3.11.20210602031702",
|
||||
"scratch-gui": "0.1.0-prerelease.20210609041525",
|
||||
"scratch-l10n": "3.11.20210609031630",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
"style-loader": "0.12.3",
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
padding: 1.75rem 3rem 2rem;
|
||||
margin: .5rem 0 2.25rem;
|
||||
background-color: $ui-blue-10percent;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
|
|
@ -121,6 +121,18 @@ module.exports.refreshSessionWithRetry = () => (dispatch => {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports.updateMuteStatus = muteStatus => ((dispatch, getState) => {
|
||||
const session = getState().session.session;
|
||||
const newSession = {
|
||||
...session,
|
||||
permissions: {
|
||||
...session.permissions,
|
||||
mute_status: muteStatus
|
||||
}
|
||||
};
|
||||
dispatch(module.exports.setSession(newSession));
|
||||
});
|
||||
|
||||
// Selectors
|
||||
module.exports.selectIsLoggedIn = state => !!get(state, ['session', 'session', 'user'], false);
|
||||
module.exports.selectUsername = state => get(state, ['session', 'session', 'user', 'username'], null);
|
||||
|
@ -128,6 +140,8 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user',
|
|||
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
|
||||
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
|
||||
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.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();
|
||||
|
|
|
@ -173,13 +173,14 @@ const mutateFollowingStudio = shouldFollow => ((dispatch, getState) => {
|
|||
});
|
||||
|
||||
const mutateStudioImage = input => ((dispatch, getState) => {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
if (!input.files || !input.files[0]) return Promise.reject(new Error('no file'));
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const currentImage = selectStudioImage(state);
|
||||
dispatch(startMutation('image'));
|
||||
if (input.files[0].size && input.files[0].size > MAX_IMAGE_BYTES) {
|
||||
return dispatch(completeMutation('image', currentImage, Errors.THUMBNAIL_TOO_LARGE));
|
||||
dispatch(completeMutation('image', currentImage, Errors.THUMBNAIL_TOO_LARGE));
|
||||
return Promise.reject(new Error('thumbnail too large'));
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', input.files[0]);
|
||||
|
|
|
@ -285,7 +285,7 @@
|
|||
}
|
||||
|
||||
.thread-limit-status {
|
||||
width: calc(100% - 10rem);
|
||||
width: calc(100% - 4rem);
|
||||
margin-left: auto;
|
||||
|
||||
.comment-status-icon {
|
||||
|
|
|
@ -17,7 +17,7 @@ const formatTime = require('../../../lib/format-time');
|
|||
const connect = require('react-redux').connect;
|
||||
|
||||
const api = require('../../../lib/api');
|
||||
const {selectMuteStatus} = require('../../../redux/session.js');
|
||||
const {selectMuteStatus, updateMuteStatus} = require('../../../redux/session.js');
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
|
@ -118,6 +118,7 @@ class ComposeComment extends React.Component {
|
|||
showWarning = body.status.mute_status.showWarning;
|
||||
muteType = body.status.mute_status.currentMessageType;
|
||||
this.setupMuteExpirationTimeout(muteExpiresAtMs);
|
||||
this.props.dispatch(updateMuteStatus(body.status.mute_status));
|
||||
}
|
||||
// Note: does not reset the message state
|
||||
this.setState({
|
||||
|
@ -425,6 +426,7 @@ class ComposeComment extends React.Component {
|
|||
|
||||
ComposeComment.propTypes = {
|
||||
commenteeId: PropTypes.number,
|
||||
dispatch: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
muteStatus: PropTypes.shape({
|
||||
offenses: PropTypes.array,
|
||||
|
|
|
@ -103,13 +103,14 @@ class TopLevelComment extends React.Component {
|
|||
replies,
|
||||
postURI,
|
||||
threadHasReplyStatus,
|
||||
totalReplyCount,
|
||||
visibility
|
||||
} = this.props;
|
||||
|
||||
const parentVisible = visibility === 'visible';
|
||||
|
||||
// Check whether this comment thread has reached the thread limit
|
||||
const hasReachedThreadLimit = hasThreadLimit && replies.length >= THREAD_LIMIT;
|
||||
const hasReachedThreadLimit = hasThreadLimit && totalReplyCount >= THREAD_LIMIT;
|
||||
|
||||
/*
|
||||
Check all the following conditions:
|
||||
|
@ -198,7 +199,7 @@ class TopLevelComment extends React.Component {
|
|||
{commentHasReplyStatus(reply.id, id) &&
|
||||
<CommentingStatus className="thread-limit-status">
|
||||
<p>
|
||||
<FormattedMessage id="comments.reachedThreadLimit" />
|
||||
<FormattedMessage id="comments.replyLimitReached" />
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
}
|
||||
|
@ -250,6 +251,7 @@ TopLevelComment.propTypes = {
|
|||
postURI: PropTypes.string,
|
||||
replies: PropTypes.arrayOf(PropTypes.object),
|
||||
threadHasReplyStatus: PropTypes.bool,
|
||||
totalReplyCount: PropTypes.number,
|
||||
visibility: PropTypes.string
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"project.comments.toggleOff": "Commenting off",
|
||||
"project.comments.toggleOn": "Commenting on",
|
||||
"project.comments.turnedOff": "Sorry, comment posting has been turned off for this project.",
|
||||
"project.comments.turnedOffGlobally": "Project comments across Scratch are turned off, but don't worry, your comments are saved and will be back soon.",
|
||||
"project.share.notShared": "This project is not shared — so only you can see it. Click share to let everyone see it!",
|
||||
"project.share.sharedLong": "Congratulations on sharing your project! Other people can now try it out, give comments, and remix it.",
|
||||
"project.share.sharedShort": "Your project is now shared.",
|
||||
|
|
|
@ -12,6 +12,7 @@ const GUI = require('scratch-gui').default;
|
|||
const IntlGUI = injectIntl(GUI);
|
||||
|
||||
const AdminPanel = require('../../components/adminpanel/adminpanel.jsx');
|
||||
const CommentingStatus = require('../../components/commenting-status/commenting-status.jsx');
|
||||
const decorateText = require('../../lib/decorate-text.jsx');
|
||||
const FlexRow = require('../../components/flex-row/flex-row.jsx');
|
||||
const Button = require('../../components/forms/button.jsx');
|
||||
|
@ -74,6 +75,7 @@ const PreviewPresentation = ({
|
|||
isFullScreen,
|
||||
isLoggedIn,
|
||||
isNewScratcher,
|
||||
isProjectCommentsGloballyEnabled,
|
||||
isProjectLoaded,
|
||||
isRemixing,
|
||||
isScratcher,
|
||||
|
@ -138,7 +140,6 @@ const PreviewPresentation = ({
|
|||
(!projectInfo.instructions && !projectInfo.description); // show if both are empty
|
||||
const showNotesAndCredits = editable || projectInfo.description ||
|
||||
(!projectInfo.instructions && !projectInfo.description); // show if both are empty
|
||||
|
||||
let banner;
|
||||
if (visibilityInfo.deleted) { // If both censored and deleted, prioritize deleted banner
|
||||
banner = (<Banner
|
||||
|
@ -574,72 +575,91 @@ const PreviewPresentation = ({
|
|||
</div>
|
||||
) : null}
|
||||
</FlexRow>
|
||||
|
||||
{/* Do not show the top-level comment form in single comment mode */}
|
||||
{!singleCommentId && (
|
||||
<FlexRow className="comments-root-reply">
|
||||
{projectInfo.comments_allowed ? (
|
||||
isLoggedIn ? (
|
||||
isShared && <ComposeComment
|
||||
postURI={`/proxy/comments/project/${projectId}`}
|
||||
onAddComment={onAddComment}
|
||||
/>
|
||||
) : (
|
||||
/* TODO add box for signing in to leave a comment */
|
||||
null
|
||||
)
|
||||
) : (
|
||||
<div className="comments-turned-off">
|
||||
<FormattedMessage id="project.comments.turnedOff" />
|
||||
</div>
|
||||
{isProjectCommentsGloballyEnabled ? (
|
||||
<React.Fragment>
|
||||
{/* Do not show the top-level comment form in single comment mode */}
|
||||
{!singleCommentId && (
|
||||
<FlexRow className="comments-root-reply">
|
||||
{projectInfo.comments_allowed ? (
|
||||
isLoggedIn ? (
|
||||
isShared && <ComposeComment
|
||||
postURI={`/proxy/comments/project/${projectId}`}
|
||||
onAddComment={onAddComment}
|
||||
/>
|
||||
) : (
|
||||
/* TODO add box for signing in to leave a comment */
|
||||
null
|
||||
)
|
||||
) : (
|
||||
<div className="comments-turned-off">
|
||||
<FormattedMessage id="project.comments.turnedOff" />
|
||||
</div>
|
||||
)}
|
||||
</FlexRow>
|
||||
)}
|
||||
</FlexRow>
|
||||
)}
|
||||
|
||||
<FlexRow className="comments-list">
|
||||
{comments.map(comment => (
|
||||
<TopLevelComment
|
||||
author={comment.author}
|
||||
canDelete={canDeleteComments}
|
||||
canDeleteWithoutConfirm={isAdmin}
|
||||
canReply={isLoggedIn && projectInfo.comments_allowed && isShared}
|
||||
canReport={isLoggedIn}
|
||||
canRestore={canRestoreComments}
|
||||
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={`/proxy/comments/project/${projectId}`}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={onAddComment}
|
||||
onDelete={onDeleteComment}
|
||||
onLoadMoreReplies={onLoadMoreReplies}
|
||||
onReport={onReportComment}
|
||||
onRestore={onRestoreComment}
|
||||
<FlexRow className="comments-list">
|
||||
{comments.map(comment => (
|
||||
<TopLevelComment
|
||||
author={comment.author}
|
||||
canDelete={canDeleteComments}
|
||||
canDeleteWithoutConfirm={isAdmin}
|
||||
canReply={
|
||||
isLoggedIn && projectInfo.comments_allowed && isShared
|
||||
}
|
||||
canReport={isLoggedIn}
|
||||
canRestore={canRestoreComments}
|
||||
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={`/proxy/comments/project/${projectId}`}
|
||||
replies={
|
||||
replies && replies[comment.id] ? replies[comment.id] : []
|
||||
}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={onAddComment}
|
||||
onDelete={onDeleteComment}
|
||||
onLoadMoreReplies={onLoadMoreReplies}
|
||||
onReport={onReportComment}
|
||||
onRestore={onRestoreComment}
|
||||
/>
|
||||
))}
|
||||
{moreCommentsToLoad &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</Button>
|
||||
}
|
||||
{!!singleCommentId &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onSeeAllComments}
|
||||
>
|
||||
<FormattedMessage id="general.seeAllComments" />
|
||||
</Button>
|
||||
}
|
||||
</FlexRow>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div>
|
||||
<CommentingStatus>
|
||||
<p>
|
||||
<FormattedMessage id="project.comments.turnedOffGlobally" />
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
<img
|
||||
className="comment-placeholder-img"
|
||||
src="/images/comments/comment-placeholder.png"
|
||||
/>
|
||||
))}
|
||||
{moreCommentsToLoad &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</Button>
|
||||
}
|
||||
{!!singleCommentId &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onSeeAllComments}
|
||||
>
|
||||
<FormattedMessage id="general.seeAllComments" />
|
||||
</Button>
|
||||
}
|
||||
</FlexRow>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
<FlexRow className="column">
|
||||
<RemixList
|
||||
|
@ -687,6 +707,7 @@ PreviewPresentation.propTypes = {
|
|||
isFullScreen: PropTypes.bool,
|
||||
isLoggedIn: PropTypes.bool,
|
||||
isNewScratcher: PropTypes.bool,
|
||||
isProjectCommentsGloballyEnabled: PropTypes.bool,
|
||||
isProjectLoaded: PropTypes.bool,
|
||||
isRemixing: PropTypes.bool,
|
||||
isScratcher: PropTypes.bool,
|
||||
|
|
|
@ -257,6 +257,10 @@ $stage-width: 480px;
|
|||
.comments-allowed-input {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.comment-placeholder-img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.remix-button,
|
||||
|
|
|
@ -27,6 +27,7 @@ const NotAvailable = require('../../components/not-available/not-available.jsx')
|
|||
const Meta = require('./meta.jsx');
|
||||
|
||||
const sessionActions = require('../../redux/session.js');
|
||||
import {selectProjectCommentsGloballyEnabled} from '../../redux/session';
|
||||
const navigationActions = require('../../redux/navigation.js');
|
||||
const previewActions = require('../../redux/preview.js');
|
||||
const projectCommentActions = require('../../redux/project-comment-actions.js');
|
||||
|
@ -738,6 +739,7 @@ class Preview extends React.Component {
|
|||
isFullScreen={this.props.fullScreen}
|
||||
isLoggedIn={this.props.isLoggedIn}
|
||||
isNewScratcher={this.props.isNewScratcher}
|
||||
isProjectCommentsGloballyEnabled={this.props.isProjectCommentsGloballyEnabled}
|
||||
isProjectLoaded={this.state.isProjectLoaded}
|
||||
isRemixing={this.state.isRemixing}
|
||||
isScratcher={this.props.isScratcher}
|
||||
|
@ -900,6 +902,7 @@ Preview.propTypes = {
|
|||
isAdmin: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isLoggedIn: PropTypes.bool,
|
||||
isProjectCommentsGloballyEnabled: PropTypes.bool,
|
||||
isNewScratcher: PropTypes.bool,
|
||||
isScratcher: PropTypes.bool,
|
||||
isShared: PropTypes.bool,
|
||||
|
@ -956,6 +959,7 @@ Preview.defaultProps = {
|
|||
backpackHost: process.env.BACKPACK_HOST,
|
||||
canUseBackpack: false,
|
||||
cloudHost: process.env.CLOUDDATA_HOST,
|
||||
isProjectCommentsGloballyEnabled: false,
|
||||
projectHost: process.env.PROJECT_HOST,
|
||||
sessionStatus: sessionActions.Status.NOT_FETCHED,
|
||||
user: {},
|
||||
|
@ -980,6 +984,8 @@ const mapStateToProps = state => {
|
|||
const isEditable = isLoggedIn &&
|
||||
(authorUsername === state.session.session.user.username ||
|
||||
state.permissions.admin === true);
|
||||
const areCommentsOn = state.session.session.flags && selectProjectCommentsGloballyEnabled(state);
|
||||
|
||||
|
||||
// if we don't have projectInfo, assume it's shared until we know otherwise
|
||||
const isShared = !projectInfoPresent || state.preview.projectInfo.is_published;
|
||||
|
@ -1010,6 +1016,7 @@ const mapStateToProps = state => {
|
|||
isLoggedIn: isLoggedIn,
|
||||
isAdmin: isAdmin,
|
||||
isNewScratcher: isLoggedIn && state.permissions.new_scratcher,
|
||||
isProjectCommentsGloballyEnabled: areCommentsOn,
|
||||
isScratcher: isLoggedIn && state.permissions.scratcher,
|
||||
isShared: isShared,
|
||||
loved: state.preview.loved,
|
||||
|
|
|
@ -102,6 +102,7 @@ const StudioComments = ({
|
|||
postURI={postURI}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
threadHasReplyStatus={hasReplyStatus(comment)}
|
||||
totalReplyCount={comment.reply_count}
|
||||
visibility={comment.visibility}
|
||||
onAddComment={handleNewComment}
|
||||
onDelete={handleDeleteComment}
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
|
@ -28,6 +29,11 @@ const StudioDescription = ({
|
|||
descriptionError, isFetching, isMutating, isMutedEditor, description, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
const [hideValidationMessage, setHideValidationMessage] = useState(false);
|
||||
|
||||
StudioDescription.handleClickOutside = () => {
|
||||
setHideValidationMessage(true);
|
||||
};
|
||||
|
||||
const fieldClassName = classNames('studio-description', {
|
||||
'mod-fetching': isFetching,
|
||||
|
@ -49,10 +55,12 @@ const StudioDescription = ({
|
|||
className={fieldClassName}
|
||||
disabled={isMutating || isFetching || isMutedEditor}
|
||||
defaultValue={description}
|
||||
onBlur={e => e.target.value !== description &&
|
||||
handleUpdate(e.target.value)}
|
||||
onBlur={e => {
|
||||
if (e.target.value !== description) handleUpdate(e.target.value);
|
||||
setHideValidationMessage(false);
|
||||
}}
|
||||
/>
|
||||
{descriptionError && <ValidationMessage
|
||||
{descriptionError && !hideValidationMessage && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(descriptionError)} />}
|
||||
/>}
|
||||
|
@ -71,6 +79,10 @@ const StudioDescription = ({
|
|||
);
|
||||
};
|
||||
|
||||
const clickOutsideConfig = {
|
||||
handleClickOutside: () => StudioDescription.handleClickOutside
|
||||
};
|
||||
|
||||
StudioDescription.propTypes = {
|
||||
descriptionError: PropTypes.string,
|
||||
canEditInfo: PropTypes.bool,
|
||||
|
@ -81,7 +93,7 @@ StudioDescription.propTypes = {
|
|||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
const connectedStudioDescription = connect(
|
||||
state => ({
|
||||
description: selectStudioDescription(state),
|
||||
canEditInfo: selectCanEditInfo(state),
|
||||
|
@ -94,3 +106,5 @@ export default connect(
|
|||
handleUpdate: mutateStudioDescription
|
||||
}
|
||||
)(StudioDescription);
|
||||
|
||||
export default onClickOutside(connectedStudioDescription, clickOutsideConfig);
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
|
@ -14,7 +15,6 @@ import {
|
|||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import StudioMuteEditMessage from './studio-mute-edit-message.jsx';
|
||||
|
||||
|
||||
import editIcon from './icons/edit-icon.svg';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
|
@ -43,6 +43,11 @@ const StudioImage = ({
|
|||
});
|
||||
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
const [hideValidationMessage, setHideValidationMessage] = useState(false);
|
||||
|
||||
StudioImage.handleClickOutside = () => {
|
||||
setHideValidationMessage(true);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={fieldClassName}
|
||||
|
@ -76,11 +81,13 @@ const StudioImage = ({
|
|||
accept="image/*"
|
||||
onChange={e => {
|
||||
handleUpdate(e.target)
|
||||
.catch(() => { /* errors are handled in the reducer */ })
|
||||
.then(dataUrl => setUploadPreview(dataUrl));
|
||||
e.target.value = '';
|
||||
setHideValidationMessage(false);
|
||||
}}
|
||||
/>
|
||||
{imageError && <ValidationMessage
|
||||
{imageError && !hideValidationMessage && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(imageError)} />}
|
||||
/>}
|
||||
|
@ -91,6 +98,10 @@ const StudioImage = ({
|
|||
);
|
||||
};
|
||||
|
||||
const clickOutsideConfig = {
|
||||
handleClickOutside: () => StudioImage.handleClickOutside
|
||||
};
|
||||
|
||||
StudioImage.propTypes = {
|
||||
imageError: PropTypes.string,
|
||||
canEditInfo: PropTypes.bool,
|
||||
|
@ -101,7 +112,7 @@ StudioImage.propTypes = {
|
|||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
const connectedStudioImage = connect(
|
||||
state => ({
|
||||
image: selectStudioImage(state),
|
||||
canEditInfo: selectCanEditInfo(state),
|
||||
|
@ -114,3 +125,5 @@ export default connect(
|
|||
handleUpdate: mutateStudioImage
|
||||
}
|
||||
)(StudioImage);
|
||||
|
||||
export default onClickOutside(connectedStudioImage, clickOutsideConfig);
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
|
@ -31,6 +32,11 @@ const StudioTitle = ({
|
|||
});
|
||||
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
const [hideValidationMessage, setHideValidationMessage] = useState(false);
|
||||
|
||||
StudioTitle.handleClickOutside = () => {
|
||||
setHideValidationMessage(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -45,10 +51,12 @@ const StudioTitle = ({
|
|||
disabled={isMutating || !canEditInfo || isFetching}
|
||||
defaultValue={title}
|
||||
onKeyDown={e => e.key === 'Enter' && e.target.blur()}
|
||||
onBlur={e => e.target.value !== title &&
|
||||
handleUpdate(e.target.value)}
|
||||
onBlur={e => {
|
||||
if (e.target.value !== title) handleUpdate(e.target.value);
|
||||
setHideValidationMessage(false);
|
||||
}}
|
||||
/>
|
||||
{titleError && <ValidationMessage
|
||||
{titleError && !hideValidationMessage && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(titleError)} />}
|
||||
/>}
|
||||
|
@ -61,6 +69,10 @@ const StudioTitle = ({
|
|||
);
|
||||
};
|
||||
|
||||
const clickOutsideConfig = {
|
||||
handleClickOutside: () => StudioTitle.handleClickOutside
|
||||
};
|
||||
|
||||
StudioTitle.propTypes = {
|
||||
titleError: PropTypes.string,
|
||||
canEditInfo: PropTypes.bool,
|
||||
|
@ -71,7 +83,7 @@ StudioTitle.propTypes = {
|
|||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
const connectedStudioTitle = connect(
|
||||
state => ({
|
||||
title: selectStudioTitle(state),
|
||||
canEditInfo: selectCanEditInfo(state),
|
||||
|
@ -84,3 +96,5 @@ export default connect(
|
|||
handleUpdate: mutateStudioTitle
|
||||
}
|
||||
)(StudioTitle);
|
||||
|
||||
export default onClickOutside(connectedStudioTitle, clickOutsideConfig);
|
||||
|
|
BIN
static/images/comments/comment-placeholder.png
Normal file
BIN
static/images/comments/comment-placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
Loading…
Reference in a new issue