Skip to content

Commit f35cc0b

Browse files
committed
display view counts
1 parent 7cd3ae1 commit f35cc0b

File tree

5 files changed

+174
-41
lines changed

5 files changed

+174
-41
lines changed

src/components/tw-description/description.jsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {FormattedMessage} from 'react-intl';
44

55
import styles from './description.css';
66
import reactStringReplace from 'react-string-replace';
7+
import {APP_NAME} from '../../lib/brand';
78

89
const decorate = text => {
910
// https://github.com/LLK/scratch-www/blob/25232a06bcceeaddec8fcb24fb63a44d870cf1cf/src/lib/decorate-text.jsx
@@ -41,7 +42,9 @@ const decorate = text => {
4142
const Description = ({
4243
instructions,
4344
credits,
44-
projectId
45+
projectId,
46+
totalViews,
47+
firstView
4548
}) => instructions !== 'unshared' && credits !== 'unshared' && (
4649
<div className={styles.description}>
4750
<div className={styles.projectLink}>
@@ -57,6 +60,41 @@ const Description = ({
5760
/>
5861
</a>
5962
</div>
63+
{typeof totalViews === 'number' ? (
64+
<div>
65+
{totalViews === 0 ? (
66+
<FormattedMessage
67+
defaultMessage="0 views on {APP_NAME}"
68+
description="Displays how many times the project has been viewed. Special case for zero views."
69+
id="tw.views.zero"
70+
values={{
71+
APP_NAME
72+
}}
73+
/>
74+
) : totalViews === 1 ? (
75+
<FormattedMessage
76+
defaultMessage="1 view on {APP_NAME}"
77+
// eslint-disable-next-line max-len
78+
description="Displays how many times the project has been viewed. Special case for one view."
79+
id="tw.views.one"
80+
values={{
81+
APP_NAME
82+
}}
83+
/>
84+
) : (
85+
<FormattedMessage
86+
defaultMessage="{views} views on {APP_NAME}"
87+
// eslint-disable-next-line max-len
88+
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."
89+
id="tw.views.many"
90+
values={{
91+
views: totalViews,
92+
APP_NAME
93+
}}
94+
/>
95+
)}
96+
</div>
97+
) : null}
6098
{instructions ? (
6199
<div>
62100
<h2 className={styles.header}>
@@ -90,7 +128,9 @@ const Description = ({
90128
Description.propTypes = {
91129
instructions: PropTypes.string,
92130
credits: PropTypes.string,
93-
projectId: PropTypes.string
131+
projectId: PropTypes.string,
132+
totalViews: PropTypes.number,
133+
firstView: PropTypes.instanceOf(Date)
94134
};
95135

96136
export default Description;

src/containers/tw-windchime-submitter.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class TWWindchimeSubmitter extends React.Component {
7373
TWWindchimeSubmitter.propTypes = {
7474
isEmbedded: PropTypes.bool.isRequired,
7575
isStarted: PropTypes.bool.isRequired,
76-
projectId: PropTypes.string.isRequired
76+
projectId: PropTypes.string
7777
};
7878

7979
const mapStateToProps = state => ({

src/lib/tw-project-meta-fetcher-hoc.jsx

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {connect} from 'react-redux';
44
import log from './log';
55

66
import {setProjectTitle} from '../reducers/project-title';
7-
import {setAuthor, setDescription} from '../reducers/tw';
7+
import {setAuthor, setDescription, resetViews, setViews} from '../reducers/tw';
88

99
export const fetchProjectMeta = async projectId => {
1010
const urls = [
@@ -32,6 +32,30 @@ export const fetchProjectMeta = async projectId => {
3232
throw firstError;
3333
};
3434

35+
const fetchWindchimes = async projectId => {
36+
try {
37+
const url = `https://windchimes.turbowarp.org/api/scratch/${projectId}`;
38+
const res = await fetch(url);
39+
40+
if (!res.ok) {
41+
return null;
42+
}
43+
44+
const data = await res.json();
45+
46+
// Windchimes returns dates in terms of days since 2000
47+
const epoch = Date.UTC(2000, 0, 1);
48+
const first = new Date(epoch + (data.firstDate * 60 * 60 * 24 * 1000));
49+
50+
return {
51+
total: data.total,
52+
first
53+
};
54+
} catch (e) {
55+
return null;
56+
}
57+
};
58+
3559
const getNoIndexTag = () => document.querySelector('meta[name="robots"][content="noindex"]');
3660
const setIndexable = indexable => {
3761
if (indexable) {
@@ -54,48 +78,80 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
5478
if (this.props.reduxProjectId !== prevProps.reduxProjectId) {
5579
this.props.onSetAuthor('', '');
5680
this.props.onSetDescription('', '');
81+
this.props.onResetViews();
82+
5783
const projectId = this.props.reduxProjectId;
84+
if (projectId !== '0') {
85+
this.tryFetchAuthorDescription(this.props.reduxProjectId);
86+
this.tryFetchViews(this.props.reduxProjectId);
87+
}
88+
}
89+
}
5890

59-
if (projectId === '0') {
60-
// don't try to get metadata
61-
} else {
62-
fetchProjectMeta(projectId).then(data => {
63-
// If project ID changed, ignore the results.
64-
if (this.props.reduxProjectId !== projectId) {
65-
return;
66-
}
67-
68-
const title = data.title;
69-
if (title) {
70-
this.props.onSetProjectTitle(title);
71-
}
72-
const authorName = data.author.username;
73-
const authorThumbnail = `https://trampoline.turbowarp.org/avatars/${data.author.id}`;
74-
this.props.onSetAuthor(authorName, authorThumbnail);
75-
const instructions = data.instructions || '';
76-
const credits = data.description || '';
77-
if (instructions || credits) {
78-
this.props.onSetDescription(instructions, credits);
79-
}
80-
setIndexable(true);
81-
})
82-
.catch(err => {
83-
setIndexable(false);
84-
if (`${err}`.includes('unshared')) {
85-
this.props.onSetDescription('unshared', 'unshared');
86-
}
87-
log.warn('cannot fetch project meta', err);
88-
});
91+
async tryFetchAuthorDescription (projectId) {
92+
try {
93+
const data = await fetchProjectMeta(projectId);
94+
95+
if (this.props.reduxProjectId !== projectId) {
96+
// If project ID changed, ignore the results.
97+
return;
8998
}
99+
100+
const title = data.title;
101+
if (title) {
102+
this.props.onSetProjectTitle(title);
103+
}
104+
const authorName = data.author.username;
105+
const authorThumbnail = `https://trampoline.turbowarp.org/avatars/${data.author.id}`;
106+
this.props.onSetAuthor(authorName, authorThumbnail);
107+
108+
const instructions = data.instructions || '';
109+
const credits = data.description || '';
110+
if (instructions || credits) {
111+
this.props.onSetDescription(instructions, credits);
112+
}
113+
114+
setIndexable(true);
115+
} catch (err) {
116+
if (`${err}`.includes('unshared')) {
117+
this.props.onSetDescription('unshared', 'unshared');
118+
}
119+
120+
setIndexable(false);
121+
122+
log.warn('cannot fetch project meta', err);
123+
}
124+
}
125+
126+
async tryFetchViews (projectId) {
127+
try {
128+
const data = await fetchWindchimes(projectId);
129+
130+
if (this.props.reduxProjectId !== projectId) {
131+
// If project ID changed, ignore the results.
132+
return;
133+
}
134+
135+
if (!data) {
136+
// No view information available
137+
return;
138+
}
139+
140+
this.props.onSetViews(data.total, data.first);
141+
} catch (err) {
142+
log.warn('cannot fetch windchimes', err);
90143
}
91144
}
145+
92146
render () {
93147
const {
94148
/* eslint-disable no-unused-vars */
95149
reduxProjectId,
96150
onSetAuthor,
97151
onSetDescription,
98152
onSetProjectTitle,
153+
onResetViews,
154+
onSetViews,
99155
/* eslint-enable no-unused-vars */
100156
...props
101157
} = this.props;
@@ -110,7 +166,9 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
110166
reduxProjectId: PropTypes.string,
111167
onSetAuthor: PropTypes.func,
112168
onSetDescription: PropTypes.func,
113-
onSetProjectTitle: PropTypes.func
169+
onSetProjectTitle: PropTypes.func,
170+
onResetViews: PropTypes.func,
171+
onSetViews: PropTypes.func
114172
};
115173
const mapStateToProps = state => ({
116174
reduxProjectId: state.scratchGui.projectState.projectId
@@ -124,7 +182,9 @@ const TWProjectMetaFetcherHOC = function (WrappedComponent) {
124182
instructions,
125183
credits
126184
})),
127-
onSetProjectTitle: title => dispatch(setProjectTitle(title))
185+
onSetProjectTitle: title => dispatch(setProjectTitle(title)),
186+
onResetViews: () => dispatch(resetViews()),
187+
onSetViews: (total, first) => dispatch(setViews(total, first))
128188
});
129189
return connect(
130190
mapStateToProps,

src/playground/render-interface.jsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ class Interface extends React.Component {
217217
isPlayerOnly,
218218
isRtl,
219219
projectId,
220+
views,
220221
/* eslint-enable no-unused-vars */
221222
...props
222223
} = this.props;
@@ -316,12 +317,14 @@ class Interface extends React.Component {
316317
<CloudVariableBadge />
317318
</div>
318319
)}
319-
{description.instructions || description.credits ? (
320+
{description.instructions || description.credits || views || projectId !== '0' ? (
320321
<div className={styles.section}>
321322
<Description
322323
instructions={description.instructions}
323324
credits={description.credits}
324325
projectId={projectId}
326+
totalViews={views && views.total}
327+
firstView={views && views.first}
325328
/>
326329
</div>
327330
) : null}
@@ -365,7 +368,11 @@ Interface.propTypes = {
365368
isLoading: PropTypes.bool,
366369
isPlayerOnly: PropTypes.bool,
367370
isRtl: PropTypes.bool,
368-
projectId: PropTypes.string
371+
projectId: PropTypes.string,
372+
views: PropTypes.shape({
373+
total: PropTypes.number,
374+
first: PropTypes.instanceOf(Date)
375+
})
369376
};
370377

371378
const mapStateToProps = state => ({
@@ -376,7 +383,8 @@ const mapStateToProps = state => ({
376383
isLoading: getIsLoading(state.scratchGui.projectState.loadingState),
377384
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
378385
isRtl: state.locales.isRtl,
379-
projectId: state.scratchGui.projectState.projectId
386+
projectId: state.scratchGui.projectState.projectId,
387+
views: state.scratchGui.tw.views
380388
});
381389

382390
const mapDispatchToProps = () => ({});

src/reducers/tw.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const SET_HAS_CLOUD_VARIABLES = 'tw/SET_HAS_CLOUD_VARIABLES';
1717
const SET_CLOUD_HOST = 'tw/SET_CLOUD_HOST';
1818
const SET_PLATFORM_MISMATCH_DETAILS = 'tw/SET_PLATFORM_MISMATCH_DETAILS';
1919
const SET_PROJECT_ERROR = 'tw/SET_PROJECT_ERROR';
20+
const SET_VIEWS = 'tw/SET_VIEWS';
2021

2122
export const initialState = {
2223
framerate: 30,
@@ -52,7 +53,8 @@ export const initialState = {
5253
platform: null,
5354
callback: null
5455
},
55-
projectError: null
56+
projectError: null,
57+
views: null
5658
};
5759

5860
const reducer = function (state, action) {
@@ -140,6 +142,10 @@ const reducer = function (state, action) {
140142
return Object.assign({}, state, {
141143
projectError: action.projectError
142144
});
145+
case SET_VIEWS:
146+
return Object.assign({}, state, {
147+
views: action.views
148+
});
143149
default:
144150
return state;
145151
}
@@ -278,6 +284,23 @@ const setProjectError = function (projectError) {
278284
};
279285
};
280286

287+
const setViews = function (total, first) {
288+
return {
289+
type: SET_VIEWS,
290+
views: {
291+
total,
292+
first
293+
}
294+
};
295+
};
296+
297+
const resetViews = function () {
298+
return {
299+
type: SET_VIEWS,
300+
views: null
301+
};
302+
};
303+
281304
export {
282305
reducer as default,
283306
initialState as twInitialState,
@@ -299,5 +322,7 @@ export {
299322
setHasCloudVariables,
300323
setCloudHost,
301324
setPlatformMismatchDetails,
302-
setProjectError
325+
setProjectError,
326+
setViews,
327+
resetViews
303328
};

0 commit comments

Comments
 (0)