diff --git a/src/components/tw-description/description.jsx b/src/components/tw-description/description.jsx
index 789baf74ab8..1c461860e6d 100644
--- a/src/components/tw-description/description.jsx
+++ b/src/components/tw-description/description.jsx
@@ -4,6 +4,7 @@ import {FormattedMessage} from 'react-intl';
import styles from './description.css';
import reactStringReplace from 'react-string-replace';
+import {APP_NAME} from '../../lib/brand';
const decorate = text => {
// https://github.com/LLK/scratch-www/blob/25232a06bcceeaddec8fcb24fb63a44d870cf1cf/src/lib/decorate-text.jsx
@@ -41,7 +42,9 @@ const decorate = text => {
const Description = ({
instructions,
credits,
- projectId
+ projectId,
+ totalViews,
+ firstView
}) => instructions !== 'unshared' && credits !== 'unshared' && (
@@ -57,6 +60,41 @@ const Description = ({
/>
+ {typeof totalViews === 'number' ? (
+
+ {totalViews === 0 ? (
+
+ ) : totalViews === 1 ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
{instructions ? (
@@ -90,7 +128,9 @@ const Description = ({
Description.propTypes = {
instructions: PropTypes.string,
credits: PropTypes.string,
- projectId: PropTypes.string
+ projectId: PropTypes.string,
+ totalViews: PropTypes.number,
+ firstView: PropTypes.instanceOf(Date)
};
export default Description;
diff --git a/src/containers/tw-windchime-submitter.jsx b/src/containers/tw-windchime-submitter.jsx
index 0053ee2f32b..4f69f19cee4 100644
--- a/src/containers/tw-windchime-submitter.jsx
+++ b/src/containers/tw-windchime-submitter.jsx
@@ -73,7 +73,7 @@ class TWWindchimeSubmitter extends React.Component {
TWWindchimeSubmitter.propTypes = {
isEmbedded: PropTypes.bool.isRequired,
isStarted: PropTypes.bool.isRequired,
- projectId: PropTypes.string.isRequired
+ projectId: PropTypes.string
};
const mapStateToProps = state => ({
diff --git a/src/lib/tw-project-meta-fetcher-hoc.jsx b/src/lib/tw-project-meta-fetcher-hoc.jsx
index b862b68e66e..6764974984a 100644
--- a/src/lib/tw-project-meta-fetcher-hoc.jsx
+++ b/src/lib/tw-project-meta-fetcher-hoc.jsx
@@ -4,7 +4,7 @@ import {connect} from 'react-redux';
import log from './log';
import {setProjectTitle} from '../reducers/project-title';
-import {setAuthor, setDescription} from '../reducers/tw';
+import {setAuthor, setDescription, resetViews, setViews} from '../reducers/tw';
export const fetchProjectMeta = async projectId => {
const urls = [
@@ -32,6 +32,30 @@ export const fetchProjectMeta = async projectId => {
throw firstError;
};
+const fetchWindchimes = async projectId => {
+ try {
+ const url = `https://windchimes.turbowarp.org/api/scratch/${projectId}`;
+ const res = await fetch(url);
+
+ if (!res.ok) {
+ return null;
+ }
+
+ const data = await res.json();
+
+ // Windchimes returns dates in terms of days since 2000
+ const epoch = Date.UTC(2000, 0, 1);
+ const first = new Date(epoch + (data.firstDate * 60 * 60 * 24 * 1000));
+
+ return {
+ total: data.total,
+ first
+ };
+ } catch (e) {
+ return null;
+ }
+};
+
const getNoIndexTag = () => document.querySelector('meta[name="robots"][content="noindex"]');
const setIndexable = indexable => {
if (indexable) {
@@ -54,41 +78,71 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
if (this.props.reduxProjectId !== prevProps.reduxProjectId) {
this.props.onSetAuthor('', '');
this.props.onSetDescription('', '');
+ this.props.onResetViews();
+
const projectId = this.props.reduxProjectId;
+ if (projectId !== '0') {
+ this.tryFetchAuthorDescription(this.props.reduxProjectId);
+ this.tryFetchViews(this.props.reduxProjectId);
+ }
+ }
+ }
- if (projectId === '0') {
- // don't try to get metadata
- } else {
- fetchProjectMeta(projectId).then(data => {
- // If project ID changed, ignore the results.
- if (this.props.reduxProjectId !== projectId) {
- return;
- }
-
- const title = data.title;
- if (title) {
- this.props.onSetProjectTitle(title);
- }
- const authorName = data.author.username;
- const authorThumbnail = `https://trampoline.turbowarp.org/avatars/${data.author.id}`;
- this.props.onSetAuthor(authorName, authorThumbnail);
- const instructions = data.instructions || '';
- const credits = data.description || '';
- if (instructions || credits) {
- this.props.onSetDescription(instructions, credits);
- }
- setIndexable(true);
- })
- .catch(err => {
- setIndexable(false);
- if (`${err}`.includes('unshared')) {
- this.props.onSetDescription('unshared', 'unshared');
- }
- log.warn('cannot fetch project meta', err);
- });
+ async tryFetchAuthorDescription (projectId) {
+ try {
+ const data = await fetchProjectMeta(projectId);
+
+ if (this.props.reduxProjectId !== projectId) {
+ // If project ID changed, ignore the results.
+ return;
}
+
+ const title = data.title;
+ if (title) {
+ this.props.onSetProjectTitle(title);
+ }
+ const authorName = data.author.username;
+ const authorThumbnail = `https://trampoline.turbowarp.org/avatars/${data.author.id}`;
+ this.props.onSetAuthor(authorName, authorThumbnail);
+
+ const instructions = data.instructions || '';
+ const credits = data.description || '';
+ if (instructions || credits) {
+ this.props.onSetDescription(instructions, credits);
+ }
+
+ setIndexable(true);
+ } catch (err) {
+ if (`${err}`.includes('unshared')) {
+ this.props.onSetDescription('unshared', 'unshared');
+ }
+
+ setIndexable(false);
+
+ log.warn('cannot fetch project meta', err);
+ }
+ }
+
+ async tryFetchViews (projectId) {
+ try {
+ const data = await fetchWindchimes(projectId);
+
+ if (this.props.reduxProjectId !== projectId) {
+ // If project ID changed, ignore the results.
+ return;
+ }
+
+ if (!data) {
+ // No view information available
+ return;
+ }
+
+ this.props.onSetViews(data.total, data.first);
+ } catch (err) {
+ log.warn('cannot fetch windchimes', err);
}
}
+
render () {
const {
/* eslint-disable no-unused-vars */
@@ -96,6 +150,8 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
onSetAuthor,
onSetDescription,
onSetProjectTitle,
+ onResetViews,
+ onSetViews,
/* eslint-enable no-unused-vars */
...props
} = this.props;
@@ -110,7 +166,9 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
reduxProjectId: PropTypes.string,
onSetAuthor: PropTypes.func,
onSetDescription: PropTypes.func,
- onSetProjectTitle: PropTypes.func
+ onSetProjectTitle: PropTypes.func,
+ onResetViews: PropTypes.func,
+ onSetViews: PropTypes.func
};
const mapStateToProps = state => ({
reduxProjectId: state.scratchGui.projectState.projectId
@@ -124,7 +182,9 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
instructions,
credits
})),
- onSetProjectTitle: title => dispatch(setProjectTitle(title))
+ onSetProjectTitle: title => dispatch(setProjectTitle(title)),
+ onResetViews: () => dispatch(resetViews()),
+ onSetViews: (total, first) => dispatch(setViews(total, first))
});
return connect(
mapStateToProps,
diff --git a/src/playground/render-interface.jsx b/src/playground/render-interface.jsx
index af3d78731f9..e006d4f3858 100644
--- a/src/playground/render-interface.jsx
+++ b/src/playground/render-interface.jsx
@@ -217,6 +217,7 @@ class Interface extends React.Component {
isPlayerOnly,
isRtl,
projectId,
+ views,
/* eslint-enable no-unused-vars */
...props
} = this.props;
@@ -316,12 +317,14 @@ class Interface extends React.Component {
)}
- {description.instructions || description.credits ? (
+ {description.instructions || description.credits || views || projectId !== '0' ? (
) : null}
@@ -365,7 +368,11 @@ Interface.propTypes = {
isLoading: PropTypes.bool,
isPlayerOnly: PropTypes.bool,
isRtl: PropTypes.bool,
- projectId: PropTypes.string
+ projectId: PropTypes.string,
+ views: PropTypes.shape({
+ total: PropTypes.number,
+ first: PropTypes.instanceOf(Date)
+ })
};
const mapStateToProps = state => ({
@@ -376,7 +383,8 @@ const mapStateToProps = state => ({
isLoading: getIsLoading(state.scratchGui.projectState.loadingState),
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
isRtl: state.locales.isRtl,
- projectId: state.scratchGui.projectState.projectId
+ projectId: state.scratchGui.projectState.projectId,
+ views: state.scratchGui.tw.views
});
const mapDispatchToProps = () => ({});
diff --git a/src/reducers/tw.js b/src/reducers/tw.js
index 332709b5256..765eb341a7c 100644
--- a/src/reducers/tw.js
+++ b/src/reducers/tw.js
@@ -17,6 +17,7 @@ const SET_HAS_CLOUD_VARIABLES = 'tw/SET_HAS_CLOUD_VARIABLES';
const SET_CLOUD_HOST = 'tw/SET_CLOUD_HOST';
const SET_PLATFORM_MISMATCH_DETAILS = 'tw/SET_PLATFORM_MISMATCH_DETAILS';
const SET_PROJECT_ERROR = 'tw/SET_PROJECT_ERROR';
+const SET_VIEWS = 'tw/SET_VIEWS';
export const initialState = {
framerate: 30,
@@ -52,7 +53,8 @@ export const initialState = {
platform: null,
callback: null
},
- projectError: null
+ projectError: null,
+ views: null
};
const reducer = function (state, action) {
@@ -140,6 +142,10 @@ const reducer = function (state, action) {
return Object.assign({}, state, {
projectError: action.projectError
});
+ case SET_VIEWS:
+ return Object.assign({}, state, {
+ views: action.views
+ });
default:
return state;
}
@@ -278,6 +284,23 @@ const setProjectError = function (projectError) {
};
};
+const setViews = function (total, first) {
+ return {
+ type: SET_VIEWS,
+ views: {
+ total,
+ first
+ }
+ };
+};
+
+const resetViews = function () {
+ return {
+ type: SET_VIEWS,
+ views: null
+ };
+};
+
export {
reducer as default,
initialState as twInitialState,
@@ -299,5 +322,7 @@ export {
setHasCloudVariables,
setCloudHost,
setPlatformMismatchDetails,
- setProjectError
+ setProjectError,
+ setViews,
+ resetViews
};