Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/components/tw-description/description.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,7 +42,9 @@ const decorate = text => {
const Description = ({
instructions,
credits,
projectId
projectId,
totalViews,
firstView
}) => instructions !== 'unshared' && credits !== 'unshared' && (
<div className={styles.description}>
<div className={styles.projectLink}>
Expand All @@ -57,6 +60,41 @@ const Description = ({
/>
</a>
</div>
{typeof totalViews === 'number' ? (
<div>
{totalViews === 0 ? (
<FormattedMessage
defaultMessage="0 views on {APP_NAME}"
description="Displays how many times the project has been viewed. Special case for zero views."
id="tw.views.zero"
values={{
APP_NAME
}}
/>
) : totalViews === 1 ? (
<FormattedMessage
defaultMessage="1 view on {APP_NAME}"
// eslint-disable-next-line max-len
description="Displays how many times the project has been viewed. Special case for one view."
id="tw.views.one"
values={{
APP_NAME
}}
/>
) : (
<FormattedMessage
defaultMessage="{views} views on {APP_NAME}"
// eslint-disable-next-line max-len
description="Displays how many times the project has been viewed. This version is used for 2 or more views. If your language differentiates between 'few' and 'many' then go with the 'many' form since that'll be most common. {views} is number of views."
id="tw.views.many"
values={{
views: totalViews,
APP_NAME
}}
/>
)}
</div>
) : null}
{instructions ? (
<div>
<h2 className={styles.header}>
Expand Down Expand Up @@ -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;
2 changes: 1 addition & 1 deletion src/containers/tw-windchime-submitter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ({
Expand Down
126 changes: 93 additions & 33 deletions src/lib/tw-project-meta-fetcher-hoc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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) {
Expand All @@ -54,48 +78,80 @@ 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 */
reduxProjectId,
onSetAuthor,
onSetDescription,
onSetProjectTitle,
onResetViews,
onSetViews,
/* eslint-enable no-unused-vars */
...props
} = this.props;
Expand All @@ -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
Expand All @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions src/playground/render-interface.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ class Interface extends React.Component {
isPlayerOnly,
isRtl,
projectId,
views,
/* eslint-enable no-unused-vars */
...props
} = this.props;
Expand Down Expand Up @@ -316,12 +317,14 @@ class Interface extends React.Component {
<CloudVariableBadge />
</div>
)}
{description.instructions || description.credits ? (
{description.instructions || description.credits || views || projectId !== '0' ? (
<div className={styles.section}>
<Description
instructions={description.instructions}
credits={description.credits}
projectId={projectId}
totalViews={views && views.total}
firstView={views && views.first}
/>
</div>
) : null}
Expand Down Expand Up @@ -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 => ({
Expand All @@ -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 = () => ({});
Expand Down
29 changes: 27 additions & 2 deletions src/reducers/tw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,7 +53,8 @@ export const initialState = {
platform: null,
callback: null
},
projectError: null
projectError: null,
views: null
};

const reducer = function (state, action) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -299,5 +322,7 @@ export {
setHasCloudVariables,
setCloudHost,
setPlatformMismatchDetails,
setProjectError
setProjectError,
setViews,
resetViews
};